mirror of
https://github.com/syssecfsu/witty.git
synced 2024-12-25 04:02:36 +01:00
commit
dd750d0c19
66
README.md
66
README.md
@ -1,44 +1,52 @@
|
|||||||
# Web Terminal
|
# Web-based Terminal Emulator
|
||||||
A (unsafe) technical demo to export a shell to web browser.
|
This program allows you to use terminal in the browser. Simply run the program and give it the command to execute when users connect via the browser. ___Interestingly___, it allows others to view your interactive sessions as well. This could be useful to provide remote support and/or help. You can use the program to run any command line programs, such as ```bash```, ```htop```, ```vi```, ```ssh```. This following screenshot shows that six interactive session running by the server. <img src="https://github.com/syssecfsu/web_terminal/blob/master/extra/main.png?raw=true" width="800px">
|
||||||
It is just a simple demo in case some people are interested in
|
|
||||||
how to setup xterm.js with websocket.
|
To use the program, you need to provide a TLS cert. You can request a free [Let's Encrypt](https://letsencrypt.org/) cert or use a self-signed cert. The program currently does not support user authentication. Therefore, do not run it in untrusted networks or leave it running. A probably safe use of the program is to run ```ssh```. Please ensure that you do not automatically login to the ssh server (e.g., via key authentication).
|
||||||
|
|
||||||
|
___AGAIN, Do NOT run this in an untrusted network. You will expose your
|
||||||
|
shell to anyone that can access your network and Do NOT leave
|
||||||
|
the server running.___
|
||||||
|
|
||||||
This program is written in the go programming language, using the
|
This program is written in the go programming language, using the
|
||||||
Gin web framework, gorilla/websocket, pty, and xterm.js!
|
Gin web framework, gorilla/websocket, pty, and xterm.js!
|
||||||
The workflow is simple, the client will initiate a terminal
|
The workflow is simple, the client will initiate a terminal
|
||||||
window (xterm.js) and create a websocket with the server. On
|
window (xterm.js) and create a websocket with the server, which relays the data between pty and xterm.
|
||||||
the server side, it serves the basic HTML/JS/CSS files and
|
|
||||||
websockets (by shovling the data between pty and xterm).
|
|
||||||
|
|
||||||
___It is amazing what you can do with 270 lines of go code.___
|
|
||||||
|
|
||||||
To use the program, download/clone the code, and in the web_terminal
|
|
||||||
directory, run ```go build .```, this will create the binary called
|
|
||||||
web_terminal. Then, go to the tls directory and create a self-signed
|
|
||||||
certificate according to the instructions in README.
|
|
||||||
|
|
||||||
To run it, use ```./web_terminal cmd options_to_cmd```.
|
|
||||||
If no cmd and options are given, web_terminal will run bash by default.
|
|
||||||
You can run shells but also single programs, such as htop. For example,
|
|
||||||
you can export the ssh shell, such as ```./web_terminal ssh 192.168.1.2 -l pi```.
|
|
||||||
|
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
The program
|
1. Install the [go](https://go.dev/) compiler.
|
||||||
has been tested on Linux, WSL2, Raspberry Pi 3B (Debian), and MacOSX.
|
2. Download the release and unzip it, or clone the repo
|
||||||
|
|
||||||
***known bug***
|
```git clone https://github.com/syssecfsu/web_terminal.git```
|
||||||
|
|
||||||
On MacOS X, running zsh with web_terminal will produce an extra %
|
3. Go to the ```tls``` directory and create a self-signed cert
|
||||||
each time in Google Chrome. Consider it a ___feautre___, will not
|
|
||||||
fix unless there is a pull request. Safari works fine though.
|
|
||||||
|
|
||||||
|
\# Generate a private key for a curve
|
||||||
|
|
||||||
**NOTE**
|
```openssl ecparam -name prime256v1 -genkey -noout -out private-key.pem```
|
||||||
|
|
||||||
___Do NOT run this in an untrusted network. You will expose your
|
\# Create a self-signed certificate
|
||||||
shell to anyone that can access your network and Do NOT leave
|
|
||||||
the server running.___
|
```openssl req -new -x509 -key private-key.pem -out cert.pem -days 360```
|
||||||
|
|
||||||
|
4. Return to the root directory of the source code and build the program
|
||||||
|
|
||||||
|
```go build .```
|
||||||
|
|
||||||
|
5. Start the server and give it the command to run. The server listens on 8080, for example:
|
||||||
|
|
||||||
|
```./web_terminal htop``` or
|
||||||
|
|
||||||
|
```./web_terminal ssh <your_server_ip> -l <user_name>```
|
||||||
|
|
||||||
|
6. Connect to the server, for example
|
||||||
|
|
||||||
|
```https://your_ip_address:8080```
|
||||||
|
|
||||||
|
The program has been tested on Linux, WSL2, Raspberry Pi 3B (Debian), and MacOSX using Google Chrome, Firefox, and Safari.
|
||||||
|
|
||||||
|
## An Screencast featuring older version of web_terminal
|
||||||
|
|
||||||
Here is a screencast for sshing into Raspberry Pi running
|
Here is a screencast for sshing into Raspberry Pi running
|
||||||
[pi-hole](https://pi-hole.net/)
|
[pi-hole](https://pi-hole.net/)
|
||||||
|
7
assets/external/bootstrap.min.css
vendored
Normal file
7
assets/external/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
assets/external/bootstrap.min.css.map
vendored
Normal file
1
assets/external/bootstrap.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
7
assets/external/bootstrap.min.js
vendored
Normal file
7
assets/external/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
assets/external/bootstrap.min.js.map
vendored
Normal file
1
assets/external/bootstrap.min.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -1,46 +1,51 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<meta name="description" content="">
|
||||||
|
<meta name="author" content="">
|
||||||
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<title>Web Terminal</title>
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Fira+Code&family=Fira+Sans&display=swap" rel="stylesheet">
|
|
||||||
|
|
||||||
<script src="xterm.js"></script>
|
<!-- Bootstrap core CSS -->
|
||||||
<script src="xterm-addon-attach.js"></script>
|
<link href="/assets/external/bootstrap.min.css" rel="stylesheet">
|
||||||
<script src="xterm-addon-fit.js"></script>
|
<link href="/assets/main.css" rel="stylesheet">
|
||||||
<script src="xterm-addon-web-links.js"></script>
|
|
||||||
|
|
||||||
<script src="main.js"></script>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="xterm.css" />
|
|
||||||
<link rel="stylesheet" href="main.css" />
|
|
||||||
|
|
||||||
<title>Websocket Terminal</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<h2>Web Terminal</h2>
|
<header>
|
||||||
<div id="terminal">
|
<nav class="navbar navbar-dark bg-dark shadow-sm navbar-xs">
|
||||||
<div id="terminal_view"></div>
|
<div class="container-fluid">
|
||||||
</div>
|
<a class="navbar-brand">
|
||||||
<script>
|
<img src="/assets/logo.svg" style="margin-left: 2em;margin-right: 1em;" height="24"
|
||||||
term = createTerminal();
|
class="d-inline-block align-text-top">
|
||||||
// print something to test output and scroll
|
Web-based Terminal Emulator
|
||||||
var str = [
|
<a class="btn btn-primary btn-sm float-end" href="/new" target="_blank" role="button">New Session</a>
|
||||||
' ┌────────────────────────────────────────────────────────────────────────────┐',
|
</div>
|
||||||
' │ \x1b[32mXterm.js\x1b[0m is the frontend component that powers many terminals including, │',
|
</nav>
|
||||||
' │ \x1b[3mVS Code\x1b[0m, \x1b[3mHyper\x1b[0m and \x1b[3mTheia\x1b[0m! │',
|
</header>
|
||||||
' │ │',
|
|
||||||
' │ \x1b[34mhttps://xtermjs.org\x1b[0m (<-try to click it!) │',
|
|
||||||
' └────────────────────────────────────────────────────────────────────────────┘',
|
|
||||||
''
|
|
||||||
].join('\n\r');
|
|
||||||
|
|
||||||
term.writeln(str);
|
<main>
|
||||||
</script>
|
<div class="container" style="margin-top:1em;">
|
||||||
|
<div class="card-deck row justify-content-center">
|
||||||
|
|
||||||
|
<!-- repeat this for each interactive session -->
|
||||||
|
{{range .players}}
|
||||||
|
<div class="card shadow-sm border-info bg-light mb-3" style="width: 16rem; margin:1em;">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Interactive session</h5>
|
||||||
|
<p class="card-text">From <em>{{.Ip}}</em>, running <strong>{{.Cmd}}</strong>, session ID: <u>{{.Name}}</u>
|
||||||
|
</p>
|
||||||
|
<a class="btn btn-secondary btn-sm float-end" href="/view/{{.Name}}" target="_blank" role="button">View
|
||||||
|
Session</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
350
assets/logo.svg
Normal file
350
assets/logo.svg
Normal file
@ -0,0 +1,350 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
width="48"
|
||||||
|
height="36"
|
||||||
|
viewBox="0 0 48 36"
|
||||||
|
fill="none"
|
||||||
|
version="1.1"
|
||||||
|
id="svg136"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M 0,7 H 16 V 0 H 2 C 0.9,0 0,0.9 0,2 Z"
|
||||||
|
fill="#cccccc"
|
||||||
|
id="path2" />
|
||||||
|
<path
|
||||||
|
d="M 32,0 H 16 v 7 h 16 z"
|
||||||
|
fill="#999999"
|
||||||
|
id="path4" />
|
||||||
|
<path
|
||||||
|
d="M 48,7 H 32 V 0 h 14 c 1.1,0 2,0.9 2,2 z"
|
||||||
|
fill="#666666"
|
||||||
|
id="path6" />
|
||||||
|
<path
|
||||||
|
d="M 46,36 H 2 C 0.9,36 0,35.1 0,34 V 6 h 48 v 28 c 0,1.1 -0.9,2 -2,2 z"
|
||||||
|
fill="url(#paint0_linear)"
|
||||||
|
id="path8"
|
||||||
|
style="fill:url(#paint0_linear)" />
|
||||||
|
<g
|
||||||
|
filter="url(#filter0_dd)"
|
||||||
|
id="g23"
|
||||||
|
transform="translate(0,-6)">
|
||||||
|
<path
|
||||||
|
d="m 15.2,24.3 -8.80001,8.8 c -0.5,0.5 -0.5,1.2 0,1.6 l 1.8,1.8 C 8.69999,37 9.4,37 9.8,36.5 l 8.8,-8.8 c 0.5,-0.5 0.5,-1.2 0,-1.6 l -1.8,-1.8 c -0.4,-0.4 -1.2,-0.4 -1.6,0 z"
|
||||||
|
fill="url(#paint1_linear)"
|
||||||
|
id="path10"
|
||||||
|
style="fill:url(#paint1_linear)" />
|
||||||
|
<mask
|
||||||
|
id="mask0"
|
||||||
|
mask-type="alpha"
|
||||||
|
maskUnits="userSpaceOnUse"
|
||||||
|
x="6"
|
||||||
|
y="24"
|
||||||
|
width="13"
|
||||||
|
height="13">
|
||||||
|
<path
|
||||||
|
d="m 15.2,24.3 -8.80001,8.8 c -0.5,0.5 -0.5,1.2 0,1.6 l 1.8,1.8 C 8.69999,37 9.4,37 9.8,36.5 l 8.8,-8.8 c 0.5,-0.5 0.5,-1.2 0,-1.6 l -1.8,-1.8 c -0.4,-0.4 -1.2,-0.4 -1.6,0 z"
|
||||||
|
fill="url(#paint2_linear)"
|
||||||
|
id="path12" />
|
||||||
|
</mask>
|
||||||
|
<g
|
||||||
|
mask="url(#mask0)"
|
||||||
|
id="g19">
|
||||||
|
<g
|
||||||
|
filter="url(#filter1_dd)"
|
||||||
|
id="g17">
|
||||||
|
<path
|
||||||
|
d="m 9.8,17.3 8.8,8.8 c 0.5,0.5 0.5,1.2 0,1.6 l -1.8,1.8 c -0.5,0.5 -1.2,0.5 -1.6,0 L 6.39999,20.7 c -0.5,-0.5 -0.5,-1.2 0,-1.6 l 1.8,-1.8 C 8.59999,16.9 9.4,16.9 9.8,17.3 Z"
|
||||||
|
fill="url(#paint3_linear)"
|
||||||
|
id="path15"
|
||||||
|
style="fill:url(#paint3_linear)" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<path
|
||||||
|
d="m 9.8,17.3 8.8,8.8 c 0.5,0.5 0.5,1.2 0,1.6 l -1.8,1.8 c -0.5,0.5 -1.2,0.5 -1.6,0 L 6.39999,20.7 c -0.5,-0.5 -0.5,-1.2 0,-1.6 l 1.8,-1.8 C 8.59999,16.9 9.4,16.9 9.8,17.3 Z"
|
||||||
|
fill="url(#paint4_linear)"
|
||||||
|
id="path21"
|
||||||
|
style="fill:url(#paint4_linear)" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
filter="url(#filter2_dd)"
|
||||||
|
id="g27"
|
||||||
|
transform="translate(0,-6)">
|
||||||
|
<path
|
||||||
|
d="M 40,32 H 24 c -0.6,0 -1,0.4 -1,1 v 3 c 0,0.6 0.4,1 1,1 h 16 c 0.6,0 1,-0.4 1,-1 v -3 c 0,-0.6 -0.4,-1 -1,-1 z"
|
||||||
|
fill="url(#paint5_linear)"
|
||||||
|
id="path25"
|
||||||
|
style="fill:url(#paint5_linear)" />
|
||||||
|
</g>
|
||||||
|
<defs
|
||||||
|
id="defs134">
|
||||||
|
<filter
|
||||||
|
id="filter0_dd"
|
||||||
|
x="3.0249901"
|
||||||
|
y="15"
|
||||||
|
width="18.950001"
|
||||||
|
height="25.875"
|
||||||
|
filterUnits="userSpaceOnUse"
|
||||||
|
color-interpolation-filters="sRGB">
|
||||||
|
<feFlood
|
||||||
|
flood-opacity="0"
|
||||||
|
result="BackgroundImageFix"
|
||||||
|
id="feFlood29" />
|
||||||
|
<feColorMatrix
|
||||||
|
in="SourceAlpha"
|
||||||
|
type="matrix"
|
||||||
|
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||||
|
id="feColorMatrix31" />
|
||||||
|
<feOffset
|
||||||
|
dy="0.5"
|
||||||
|
id="feOffset33" />
|
||||||
|
<feGaussianBlur
|
||||||
|
stdDeviation="0.5"
|
||||||
|
id="feGaussianBlur35" />
|
||||||
|
<feColorMatrix
|
||||||
|
type="matrix"
|
||||||
|
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"
|
||||||
|
id="feColorMatrix37" />
|
||||||
|
<feBlend
|
||||||
|
mode="normal"
|
||||||
|
in2="BackgroundImageFix"
|
||||||
|
result="effect1_dropShadow"
|
||||||
|
id="feBlend39" />
|
||||||
|
<feColorMatrix
|
||||||
|
in="SourceAlpha"
|
||||||
|
type="matrix"
|
||||||
|
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||||
|
id="feColorMatrix41" />
|
||||||
|
<feOffset
|
||||||
|
dy="1"
|
||||||
|
id="feOffset43" />
|
||||||
|
<feGaussianBlur
|
||||||
|
stdDeviation="1.5"
|
||||||
|
id="feGaussianBlur45" />
|
||||||
|
<feColorMatrix
|
||||||
|
type="matrix"
|
||||||
|
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"
|
||||||
|
id="feColorMatrix47" />
|
||||||
|
<feBlend
|
||||||
|
mode="normal"
|
||||||
|
in2="effect1_dropShadow"
|
||||||
|
result="effect2_dropShadow"
|
||||||
|
id="feBlend49" />
|
||||||
|
<feBlend
|
||||||
|
mode="normal"
|
||||||
|
in="SourceGraphic"
|
||||||
|
in2="effect2_dropShadow"
|
||||||
|
result="shape"
|
||||||
|
id="feBlend51" />
|
||||||
|
</filter>
|
||||||
|
<filter
|
||||||
|
id="filter1_dd"
|
||||||
|
x="3.0249901"
|
||||||
|
y="15"
|
||||||
|
width="18.950001"
|
||||||
|
height="18.875"
|
||||||
|
filterUnits="userSpaceOnUse"
|
||||||
|
color-interpolation-filters="sRGB">
|
||||||
|
<feFlood
|
||||||
|
flood-opacity="0"
|
||||||
|
result="BackgroundImageFix"
|
||||||
|
id="feFlood54" />
|
||||||
|
<feColorMatrix
|
||||||
|
in="SourceAlpha"
|
||||||
|
type="matrix"
|
||||||
|
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||||
|
id="feColorMatrix56" />
|
||||||
|
<feOffset
|
||||||
|
dy="0.5"
|
||||||
|
id="feOffset58" />
|
||||||
|
<feGaussianBlur
|
||||||
|
stdDeviation="0.5"
|
||||||
|
id="feGaussianBlur60" />
|
||||||
|
<feColorMatrix
|
||||||
|
type="matrix"
|
||||||
|
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"
|
||||||
|
id="feColorMatrix62" />
|
||||||
|
<feBlend
|
||||||
|
mode="normal"
|
||||||
|
in2="BackgroundImageFix"
|
||||||
|
result="effect1_dropShadow"
|
||||||
|
id="feBlend64" />
|
||||||
|
<feColorMatrix
|
||||||
|
in="SourceAlpha"
|
||||||
|
type="matrix"
|
||||||
|
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||||
|
id="feColorMatrix66" />
|
||||||
|
<feOffset
|
||||||
|
dy="1"
|
||||||
|
id="feOffset68" />
|
||||||
|
<feGaussianBlur
|
||||||
|
stdDeviation="1.5"
|
||||||
|
id="feGaussianBlur70" />
|
||||||
|
<feColorMatrix
|
||||||
|
type="matrix"
|
||||||
|
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"
|
||||||
|
id="feColorMatrix72" />
|
||||||
|
<feBlend
|
||||||
|
mode="normal"
|
||||||
|
in2="effect1_dropShadow"
|
||||||
|
result="effect2_dropShadow"
|
||||||
|
id="feBlend74" />
|
||||||
|
<feBlend
|
||||||
|
mode="normal"
|
||||||
|
in="SourceGraphic"
|
||||||
|
in2="effect2_dropShadow"
|
||||||
|
result="shape"
|
||||||
|
id="feBlend76" />
|
||||||
|
</filter>
|
||||||
|
<filter
|
||||||
|
id="filter2_dd"
|
||||||
|
x="20"
|
||||||
|
y="30"
|
||||||
|
width="24"
|
||||||
|
height="11"
|
||||||
|
filterUnits="userSpaceOnUse"
|
||||||
|
color-interpolation-filters="sRGB">
|
||||||
|
<feFlood
|
||||||
|
flood-opacity="0"
|
||||||
|
result="BackgroundImageFix"
|
||||||
|
id="feFlood79" />
|
||||||
|
<feColorMatrix
|
||||||
|
in="SourceAlpha"
|
||||||
|
type="matrix"
|
||||||
|
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||||
|
id="feColorMatrix81" />
|
||||||
|
<feOffset
|
||||||
|
dy="0.5"
|
||||||
|
id="feOffset83" />
|
||||||
|
<feGaussianBlur
|
||||||
|
stdDeviation="0.5"
|
||||||
|
id="feGaussianBlur85" />
|
||||||
|
<feColorMatrix
|
||||||
|
type="matrix"
|
||||||
|
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"
|
||||||
|
id="feColorMatrix87" />
|
||||||
|
<feBlend
|
||||||
|
mode="normal"
|
||||||
|
in2="BackgroundImageFix"
|
||||||
|
result="effect1_dropShadow"
|
||||||
|
id="feBlend89" />
|
||||||
|
<feColorMatrix
|
||||||
|
in="SourceAlpha"
|
||||||
|
type="matrix"
|
||||||
|
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||||
|
id="feColorMatrix91" />
|
||||||
|
<feOffset
|
||||||
|
dy="1"
|
||||||
|
id="feOffset93" />
|
||||||
|
<feGaussianBlur
|
||||||
|
stdDeviation="1.5"
|
||||||
|
id="feGaussianBlur95" />
|
||||||
|
<feColorMatrix
|
||||||
|
type="matrix"
|
||||||
|
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"
|
||||||
|
id="feColorMatrix97" />
|
||||||
|
<feBlend
|
||||||
|
mode="normal"
|
||||||
|
in2="effect1_dropShadow"
|
||||||
|
result="effect2_dropShadow"
|
||||||
|
id="feBlend99" />
|
||||||
|
<feBlend
|
||||||
|
mode="normal"
|
||||||
|
in="SourceGraphic"
|
||||||
|
in2="effect2_dropShadow"
|
||||||
|
result="shape"
|
||||||
|
id="feBlend101" />
|
||||||
|
</filter>
|
||||||
|
<linearGradient
|
||||||
|
id="paint0_linear"
|
||||||
|
x1="36.446201"
|
||||||
|
y1="47.825699"
|
||||||
|
x2="11.8217"
|
||||||
|
y2="5.1747999"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="translate(0,-6)">
|
||||||
|
<stop
|
||||||
|
stop-color="#333333"
|
||||||
|
id="stop104" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#4D4D4D"
|
||||||
|
id="stop106" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="paint1_linear"
|
||||||
|
x1="14.5276"
|
||||||
|
y1="33.995899"
|
||||||
|
x2="10.4841"
|
||||||
|
y2="26.992399"
|
||||||
|
gradientUnits="userSpaceOnUse">
|
||||||
|
<stop
|
||||||
|
stop-color="#999999"
|
||||||
|
id="stop109" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#B3B3B3"
|
||||||
|
id="stop111" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="paint2_linear"
|
||||||
|
x1="14.5276"
|
||||||
|
y1="33.995899"
|
||||||
|
x2="10.4841"
|
||||||
|
y2="26.992399"
|
||||||
|
gradientUnits="userSpaceOnUse">
|
||||||
|
<stop
|
||||||
|
stop-color="#999999"
|
||||||
|
id="stop114" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#B3B3B3"
|
||||||
|
id="stop116" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="paint3_linear"
|
||||||
|
x1="16.2747"
|
||||||
|
y1="30.0336"
|
||||||
|
x2="8.73699"
|
||||||
|
y2="16.9781"
|
||||||
|
gradientUnits="userSpaceOnUse">
|
||||||
|
<stop
|
||||||
|
stop-color="#CCCCCC"
|
||||||
|
id="stop119" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#E6E6E6"
|
||||||
|
id="stop121" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="paint4_linear"
|
||||||
|
x1="16.2747"
|
||||||
|
y1="30.0336"
|
||||||
|
x2="8.73699"
|
||||||
|
y2="16.9781"
|
||||||
|
gradientUnits="userSpaceOnUse">
|
||||||
|
<stop
|
||||||
|
stop-color="#CCCCCC"
|
||||||
|
id="stop124" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#E6E6E6"
|
||||||
|
id="stop126" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="paint5_linear"
|
||||||
|
x1="35.149601"
|
||||||
|
y1="39.955299"
|
||||||
|
x2="28.850401"
|
||||||
|
y2="29.044701"
|
||||||
|
gradientUnits="userSpaceOnUse">
|
||||||
|
<stop
|
||||||
|
stop-color="#CCCCCC"
|
||||||
|
id="stop129" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
stop-color="#E6E6E6"
|
||||||
|
id="stop131" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 9.3 KiB |
@ -28,8 +28,28 @@
|
|||||||
background-color: #ffffff20;
|
background-color: #ffffff20;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
.banner {
|
||||||
font-family: 'Fira Sans', sans-serif;
|
position: absolute;
|
||||||
font-size: 32;
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
background-color: #0b0b33;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-content {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 22px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
color: #ffffffff;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navbar-xs .navbar-brand {
|
||||||
|
padding: 0px 12px;
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-xs .navbar-nav>li>a {
|
||||||
|
padding-top: 0px;
|
||||||
|
padding-bottom: 0px;
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
function createTerminal() {
|
function createTerminal(path) {
|
||||||
// vscode-snazzy https://github.com/Tyriar/vscode-snazzy
|
// vscode-snazzy https://github.com/Tyriar/vscode-snazzy
|
||||||
// copied from xterm.js website
|
// copied from xterm.js website
|
||||||
var baseTheme = {
|
var baseTheme = {
|
||||||
@ -43,7 +43,7 @@ function createTerminal() {
|
|||||||
fitAddon.fit();
|
fitAddon.fit();
|
||||||
|
|
||||||
// create the websocket and connect to the server
|
// create the websocket and connect to the server
|
||||||
const ws_uri = "wss://" + window.location.host + "/ws";
|
const ws_uri = "wss://" + window.location.host + path;
|
||||||
const socket = new WebSocket(ws_uri);
|
const socket = new WebSocket(ws_uri);
|
||||||
const attachAddon = new AttachAddon.AttachAddon(socket);
|
const attachAddon = new AttachAddon.AttachAddon(socket);
|
||||||
term.loadAddon(attachAddon);
|
term.loadAddon(attachAddon);
|
||||||
|
55
assets/term.html
Normal file
55
assets/term.html
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Fira+Code&family=Fira+Sans&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<script src="/assets/external/xterm.js"></script>
|
||||||
|
<script src="/assets/external/xterm-addon-attach.js"></script>
|
||||||
|
<script src="/assets/external/xterm-addon-fit.js"></script>
|
||||||
|
<script src="/assets/external/xterm-addon-web-links.js"></script>
|
||||||
|
|
||||||
|
<script src="/assets/main.js"></script>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/assets/external/xterm.css" />
|
||||||
|
<link rel="stylesheet" href="/assets/main.css" />
|
||||||
|
|
||||||
|
<title>{{.title}}</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="banner">
|
||||||
|
<div class="banner-content">
|
||||||
|
{{.title}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 7em;">
|
||||||
|
<div id="terminal">
|
||||||
|
<div id="terminal_view"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
term = createTerminal("{{.path}}");
|
||||||
|
// print something to test output and scroll
|
||||||
|
var str = [
|
||||||
|
' ┌────────────────────────────────────────────────────────────────────────────┐',
|
||||||
|
' │ \x1b[32mXterm.js\x1b[0m is the frontend component that powers many terminals including, │',
|
||||||
|
' │ \x1b[3mVS Code\x1b[0m, \x1b[3mHyper\x1b[0m and \x1b[3mTheia\x1b[0m! │',
|
||||||
|
' │ │',
|
||||||
|
' │ \x1b[34mhttps://xtermjs.org\x1b[0m (<-try to click it!) │',
|
||||||
|
' └────────────────────────────────────────────────────────────────────────────┘',
|
||||||
|
''
|
||||||
|
].join('\n\r');
|
||||||
|
|
||||||
|
term.writeln(str);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
BIN
extra/main.png
Normal file
BIN
extra/main.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 622 KiB |
0
extra/screencast.gif
Executable file → Normal file
0
extra/screencast.gif
Executable file → Normal file
Before Width: | Height: | Size: 955 KiB After Width: | Height: | Size: 955 KiB |
2
go.mod
2
go.mod
@ -4,6 +4,7 @@ go 1.17
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/creack/pty v1.1.17 // indirect
|
github.com/creack/pty v1.1.17 // indirect
|
||||||
|
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
github.com/gin-gonic/gin v1.7.7 // indirect
|
github.com/gin-gonic/gin v1.7.7 // indirect
|
||||||
github.com/go-playground/locales v0.13.0 // indirect
|
github.com/go-playground/locales v0.13.0 // indirect
|
||||||
@ -11,6 +12,7 @@ require (
|
|||||||
github.com/go-playground/validator/v10 v10.4.1 // indirect
|
github.com/go-playground/validator/v10 v10.4.1 // indirect
|
||||||
github.com/golang/protobuf v1.3.3 // indirect
|
github.com/golang/protobuf v1.3.3 // indirect
|
||||||
github.com/gorilla/websocket v1.4.2 // indirect
|
github.com/gorilla/websocket v1.4.2 // indirect
|
||||||
|
github.com/hashicorp/go-uuid v1.0.2 // indirect
|
||||||
github.com/json-iterator/go v1.1.9 // indirect
|
github.com/json-iterator/go v1.1.9 // indirect
|
||||||
github.com/leodido/go-urn v1.2.0 // indirect
|
github.com/leodido/go-urn v1.2.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.12 // indirect
|
github.com/mattn/go-isatty v0.0.12 // indirect
|
||||||
|
4
go.sum
4
go.sum
@ -2,6 +2,8 @@ github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
|
|||||||
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 h1:RAV05c0xOkJ3dZGS0JFybxFKZ2WMLabgx3uXnd7rpGs=
|
||||||
|
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4=
|
||||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
|
github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
|
||||||
@ -18,6 +20,8 @@ github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaW
|
|||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE=
|
||||||
|
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
|
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
|
||||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||||
|
291
main.go
291
main.go
@ -1,235 +1,58 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/creack/pty"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/syssecfsu/web_terminal/term_conn"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
// command line options
|
||||||
// Time allowed to write a message to the peer.
|
var cmdToExec = []string{"bash"}
|
||||||
writeWait = 5 * time.Second
|
|
||||||
|
|
||||||
// Time allowed to read the next pong message from the peer.
|
|
||||||
pongWait = 30 * time.Second
|
|
||||||
|
|
||||||
// Maximum message size allowed from peer.
|
|
||||||
maxMessageSize = 8192
|
|
||||||
|
|
||||||
// Send pings to peer with this period. Must be less than pongWait.
|
|
||||||
pingPeriod = (pongWait * 9) / 10
|
|
||||||
|
|
||||||
// Time to wait before force close on connection.
|
|
||||||
closeGracePeriod = 10 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
func createPty(cmdline []string) (*os.File, *exec.Cmd, error) {
|
|
||||||
// Create a shell command.
|
|
||||||
cmd := exec.Command(cmdline[0], cmdline[1:]...)
|
|
||||||
|
|
||||||
// Start the command with a pty.
|
|
||||||
ptmx, err := pty.Start(cmd)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use fixed size, the xterm is initalized as 122x37,
|
|
||||||
// But we set pty to 120x36. Using fullsize will lead
|
|
||||||
// some program to misbehaive.
|
|
||||||
pty.Setsize(ptmx, &pty.Winsize{
|
|
||||||
Cols: 120,
|
|
||||||
Rows: 36,
|
|
||||||
})
|
|
||||||
|
|
||||||
log.Printf("Create shell process %v (%v)", cmdline, cmd.Process.Pid)
|
|
||||||
return ptmx, cmd, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var host *string = nil
|
var host *string = nil
|
||||||
|
|
||||||
var upgrader = websocket.Upgrader{
|
// simple function to check origin
|
||||||
ReadBufferSize: 4096,
|
func checkOrigin(r *http.Request) bool {
|
||||||
WriteBufferSize: 4096,
|
org := r.Header.Get("Origin")
|
||||||
CheckOrigin: func(r *http.Request) bool {
|
h, err := url.Parse(org)
|
||||||
org := r.Header.Get("Origin")
|
|
||||||
h, err := url.Parse(org)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
|
||||||
|
|
||||||
if (host == nil) || (*host != h.Host) {
|
|
||||||
log.Println("Failed origin check of ", org)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (host != nil) && (*host == h.Host)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Periodically send ping message to detect the status of the ws
|
|
||||||
func ping(ws *websocket.Conn, done chan struct{}) {
|
|
||||||
ticker := time.NewTicker(pingPeriod)
|
|
||||||
defer ticker.Stop()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ticker.C:
|
|
||||||
err := ws.WriteControl(websocket.PingMessage,
|
|
||||||
[]byte{}, time.Now().Add(writeWait))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Failed to write ping message:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
case <-done:
|
|
||||||
log.Println("Exit ping routine as stdout is going away")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (host == nil) || (*host != h.Host) {
|
||||||
|
log.Println("Failed origin check of ", org)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (host != nil) && (*host == h.Host)
|
||||||
}
|
}
|
||||||
|
|
||||||
// shovel data from websocket to pty stdin
|
type InteractiveSession struct {
|
||||||
func toPtyStdin(ws *websocket.Conn, ptmx *os.File) {
|
Ip string
|
||||||
ws.SetReadLimit(maxMessageSize)
|
Cmd string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
// set the readdeadline. The idea here is simple,
|
func fillIndex(c *gin.Context) {
|
||||||
// as long as we keep receiving pong message,
|
var players []InteractiveSession
|
||||||
// the readdeadline will keep updating. Otherwise
|
|
||||||
// read will timeout.
|
term_conn.ForEachSession(func(tc *term_conn.TermConn) {
|
||||||
ws.SetReadDeadline(time.Now().Add(pongWait))
|
players = append(players, InteractiveSession{
|
||||||
ws.SetPongHandler(func(string) error {
|
Name: tc.Name,
|
||||||
ws.SetReadDeadline(time.Now().Add(pongWait))
|
Ip: tc.Ip,
|
||||||
return nil
|
Cmd: cmdToExec[0],
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
for {
|
c.HTML(http.StatusOK, "index.html", gin.H{
|
||||||
_, buf, err := ws.ReadMessage()
|
"title": "Interactive terminal",
|
||||||
|
"path": "/ws_do",
|
||||||
if err != nil {
|
"players": players,
|
||||||
log.Println("Failed to receive data from ws:", err)
|
})
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = ptmx.Write(buf)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Failed to send data to pty stdin: ", err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// shovel data from pty Stdout to WS
|
|
||||||
func fromPtyStdout(ws *websocket.Conn, ptmx *os.File, done chan struct{}) {
|
|
||||||
readBuf := make([]byte, 4096)
|
|
||||||
|
|
||||||
for {
|
|
||||||
n, err := ptmx.Read(readBuf)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Failed to read from pty stdout: ", err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
ws.SetWriteDeadline(time.Now().Add(writeWait))
|
|
||||||
if err = ws.WriteMessage(websocket.BinaryMessage, readBuf[:n]); err != nil {
|
|
||||||
log.Println("Failed to write message: ", err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
close(done)
|
|
||||||
|
|
||||||
ws.SetWriteDeadline(time.Now().Add(writeWait))
|
|
||||||
ws.WriteMessage(websocket.CloseMessage,
|
|
||||||
websocket.FormatCloseMessage(websocket.CloseNormalClosure, "Pty closed"))
|
|
||||||
time.Sleep(closeGracePeriod)
|
|
||||||
}
|
|
||||||
|
|
||||||
var cmdToExec = []string{"bash"}
|
|
||||||
|
|
||||||
// handle websockets
|
|
||||||
func wsHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ws, err := upgrader.Upgrade(w, r, nil)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Failed to create websocket: ", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer ws.Close()
|
|
||||||
|
|
||||||
log.Println("\n\nCreated the websocket")
|
|
||||||
|
|
||||||
ptmx, cmd, err := createPty(cmdToExec)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Failed to create PTY: ", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
done := make(chan struct{})
|
|
||||||
|
|
||||||
go fromPtyStdout(ws, ptmx, done)
|
|
||||||
go ping(ws, done)
|
|
||||||
|
|
||||||
toPtyStdin(ws, ptmx)
|
|
||||||
|
|
||||||
// cleanup the pty and its related process
|
|
||||||
ptmx.Close()
|
|
||||||
proc := cmd.Process
|
|
||||||
|
|
||||||
// send an interrupt, this will cause the shell process to
|
|
||||||
// return from syscalls if any is pending
|
|
||||||
if err := proc.Signal(os.Interrupt); err != nil {
|
|
||||||
log.Printf("Failed to send Interrupt to shell process(%v): %v ", proc.Pid, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for a second for shell process to interrupt before kill it
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
|
|
||||||
log.Printf("Try to kill the shell process(%v)", proc.Pid)
|
|
||||||
|
|
||||||
if err := proc.Signal(os.Kill); err != nil {
|
|
||||||
log.Printf("Failed to send KILL to shell process(%v): %v", proc.Pid, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := proc.Wait(); err != nil {
|
|
||||||
log.Printf("Failed to wait for shell process(%v): %v", proc.Pid, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// return files
|
|
||||||
func fileHandler(c *gin.Context, fname string) {
|
|
||||||
// if the URL has no fname, c.Param returns "/"
|
|
||||||
if fname == "/" {
|
|
||||||
fname = "/index.html"
|
|
||||||
host = &c.Request.Host
|
|
||||||
}
|
|
||||||
|
|
||||||
fname = fname[1:] //fname always starts with /
|
|
||||||
log.Println("Sending ", fname)
|
|
||||||
|
|
||||||
if strings.HasSuffix(fname, "html") {
|
|
||||||
c.HTML(200, fname, nil)
|
|
||||||
} else {
|
|
||||||
//c.HTML interprets the file as HTML file
|
|
||||||
//we do not need that for regular files
|
|
||||||
if strings.HasPrefix(fname, "xterm") {
|
|
||||||
c.File(fmt.Sprint("./assets/external/", fname))
|
|
||||||
} else {
|
|
||||||
c.File(fmt.Sprint("./assets/", fname))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@ -257,17 +80,43 @@ func main() {
|
|||||||
rt.SetTrustedProxies(nil)
|
rt.SetTrustedProxies(nil)
|
||||||
rt.LoadHTMLGlob("./assets/*.html")
|
rt.LoadHTMLGlob("./assets/*.html")
|
||||||
|
|
||||||
rt.GET("/*fname", func(c *gin.Context) {
|
rt.GET("/view/:sname", func(c *gin.Context) {
|
||||||
fname := c.Param("fname")
|
sname := c.Param("sname")
|
||||||
|
c.HTML(http.StatusOK, "term.html", gin.H{
|
||||||
// ws is a special case to create a new websocket
|
"title": "Viewer terminal",
|
||||||
switch fname {
|
"path": "/ws_view/" + sname,
|
||||||
case "/ws":
|
})
|
||||||
wsHandler(c.Writer, c.Request)
|
|
||||||
default:
|
|
||||||
fileHandler(c, fname)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
rt.GET("/new", func(c *gin.Context) {
|
||||||
|
if host == nil {
|
||||||
|
host = &c.Request.Host
|
||||||
|
}
|
||||||
|
|
||||||
|
c.HTML(http.StatusOK, "term.html", gin.H{
|
||||||
|
"title": "Interactive terminal",
|
||||||
|
"path": "/ws_do",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
rt.GET("/ws_do", func(c *gin.Context) {
|
||||||
|
term_conn.ConnectTerm(c.Writer, c.Request, false, "", cmdToExec)
|
||||||
|
})
|
||||||
|
|
||||||
|
rt.GET("/ws_view/:sname", func(c *gin.Context) {
|
||||||
|
path := c.Param("sname")
|
||||||
|
term_conn.ConnectTerm(c.Writer, c.Request, true, path, nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
// handle static files
|
||||||
|
rt.Static("/assets", "./assets")
|
||||||
|
|
||||||
|
rt.GET("/", func(c *gin.Context) {
|
||||||
|
host = &c.Request.Host
|
||||||
|
fillIndex(c)
|
||||||
|
})
|
||||||
|
|
||||||
|
term_conn.Init(checkOrigin)
|
||||||
|
|
||||||
rt.RunTLS(":8080", "./tls/cert.pem", "./tls/private-key.pem")
|
rt.RunTLS(":8080", "./tls/cert.pem", "./tls/private-key.pem")
|
||||||
}
|
}
|
||||||
|
68
term_conn/reg.go
Normal file
68
term_conn/reg.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package term_conn
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
// a simple registry for actors and their channels. It is possible to
|
||||||
|
// design this using channels, but it is simple enough with mutex
|
||||||
|
type Registry struct {
|
||||||
|
mtx sync.Mutex
|
||||||
|
players map[string]*TermConn
|
||||||
|
}
|
||||||
|
|
||||||
|
var registry Registry
|
||||||
|
|
||||||
|
func (reg *Registry) init() {
|
||||||
|
reg.players = make(map[string]*TermConn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Registry) addPlayer(tc *TermConn) {
|
||||||
|
d.mtx.Lock()
|
||||||
|
if _, ok := d.players[tc.Name]; ok {
|
||||||
|
log.Println(tc.Name, "Already exist in the dispatcher, skip registration")
|
||||||
|
} else {
|
||||||
|
d.players[tc.Name] = tc
|
||||||
|
log.Println("Add interactive session to registry", tc.Name)
|
||||||
|
}
|
||||||
|
d.mtx.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Registry) removePlayer(name string) error {
|
||||||
|
d.mtx.Lock()
|
||||||
|
var err error = errors.New("not found")
|
||||||
|
|
||||||
|
if _, ok := d.players[name]; ok {
|
||||||
|
delete(d.players, name)
|
||||||
|
err = nil
|
||||||
|
log.Println("Removed interactive session to registry", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.mtx.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// we do not want to return the channel to viewer so it won't be used out of the critical section
|
||||||
|
func (d *Registry) sendToPlayer(name string, ws *websocket.Conn) bool {
|
||||||
|
d.mtx.Lock()
|
||||||
|
tc, ok := d.players[name]
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
tc.vchan <- ws
|
||||||
|
}
|
||||||
|
|
||||||
|
d.mtx.Unlock()
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func ForEachSession(fp func(tc *TermConn)) {
|
||||||
|
registry.mtx.Lock()
|
||||||
|
for _, v := range registry.players {
|
||||||
|
fp(v)
|
||||||
|
}
|
||||||
|
registry.mtx.Unlock()
|
||||||
|
}
|
368
term_conn/relay.go
Normal file
368
term_conn/relay.go
Normal file
@ -0,0 +1,368 @@
|
|||||||
|
//This file contains code to relay traffic between websocket and pty
|
||||||
|
package term_conn
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/creack/pty"
|
||||||
|
"github.com/dchest/uniuri"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Time allowed to write a message to the peer.
|
||||||
|
readWait = 10 * time.Second
|
||||||
|
writeWait = 10 * time.Second
|
||||||
|
viewWait = 3 * time.Second
|
||||||
|
|
||||||
|
// Time allowed to read the next pong message from the peer.
|
||||||
|
pongWait = 10 * time.Second
|
||||||
|
|
||||||
|
// Maximum message size allowed from peer.
|
||||||
|
maxMessageSize = 4096
|
||||||
|
readBufferSize = 1024
|
||||||
|
WriteBufferSize = 1024
|
||||||
|
|
||||||
|
// Send pings to peer with this period. Must be less than pongWait.
|
||||||
|
pingPeriod = (pongWait * 9) / 10
|
||||||
|
)
|
||||||
|
|
||||||
|
var upgrader = websocket.Upgrader{
|
||||||
|
ReadBufferSize: readBufferSize,
|
||||||
|
WriteBufferSize: WriteBufferSize,
|
||||||
|
CheckOrigin: func(r *http.Request) bool {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// TermConn represents the connected websocket and pty.
|
||||||
|
// if isViewer is true
|
||||||
|
type TermConn struct {
|
||||||
|
Name string
|
||||||
|
Ip string
|
||||||
|
|
||||||
|
// only valid for doers
|
||||||
|
ws *websocket.Conn
|
||||||
|
ptmx *os.File // the pty that runs the command
|
||||||
|
cmd *exec.Cmd // represents the process, we need it to terminate the process
|
||||||
|
vchan chan *websocket.Conn // channel to receive viewers
|
||||||
|
ws_done chan struct{} // ws is closed, only close this chan in ws reader
|
||||||
|
pty_done chan struct{} // pty is closed, close this chan in pty reader
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TermConn) createPty(cmdline []string) error {
|
||||||
|
// Create a shell command.
|
||||||
|
cmd := exec.Command(cmdline[0], cmdline[1:]...)
|
||||||
|
|
||||||
|
// Start the command with a pty.
|
||||||
|
ptmx, err := pty.Start(cmd)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use fixed size, the xterm is initalized as 122x37,
|
||||||
|
// But we set pty to 120x36. Using fullsize will lead
|
||||||
|
// some program to misbehave.
|
||||||
|
pty.Setsize(ptmx, &pty.Winsize{
|
||||||
|
Cols: 120,
|
||||||
|
Rows: 36,
|
||||||
|
})
|
||||||
|
|
||||||
|
tc.ptmx = ptmx
|
||||||
|
tc.cmd = cmd
|
||||||
|
|
||||||
|
log.Printf("Create shell process %v (%v)", cmdline, cmd.Process.Pid)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Periodically send ping message to detect the status of the ws
|
||||||
|
func (tc *TermConn) ping(wg *sync.WaitGroup) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
ticker := time.NewTicker(pingPeriod)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
out:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
err := tc.ws.WriteControl(websocket.PingMessage,
|
||||||
|
[]byte{}, time.Now().Add(writeWait))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to write ping message:", err)
|
||||||
|
break out
|
||||||
|
}
|
||||||
|
case <-tc.pty_done:
|
||||||
|
log.Println("Exit ping routine as pty is going away")
|
||||||
|
break out
|
||||||
|
|
||||||
|
case <-tc.ws_done:
|
||||||
|
log.Println("Exit ping routine as ws is going away")
|
||||||
|
break out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Ping routine exited")
|
||||||
|
}
|
||||||
|
|
||||||
|
// shovel data from websocket to pty stdin
|
||||||
|
func (tc *TermConn) wsToPtyStdin(wg *sync.WaitGroup) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
tc.ws.SetReadLimit(maxMessageSize)
|
||||||
|
|
||||||
|
// set the readdeadline. The idea here is simple,
|
||||||
|
// as long as we keep receiving pong message,
|
||||||
|
// the readdeadline will keep updating. Otherwise
|
||||||
|
// read will timeout.
|
||||||
|
tc.ws.SetReadDeadline(time.Now().Add(pongWait))
|
||||||
|
tc.ws.SetPongHandler(func(string) error {
|
||||||
|
tc.ws.SetReadDeadline(time.Now().Add(pongWait))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
bufChan := make(chan []byte)
|
||||||
|
|
||||||
|
go func() { //create a goroutine to read from ws
|
||||||
|
for {
|
||||||
|
_, buf, err := tc.ws.ReadMessage()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to receive data from ws:", err)
|
||||||
|
close(bufChan) // close chan by producer
|
||||||
|
close(tc.ws_done)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
bufChan <- buf
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
// we do not need to forward user input to viewers, only the stdout
|
||||||
|
out:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case buf, ok := <-bufChan:
|
||||||
|
if !ok {
|
||||||
|
log.Println("Exit wsToPtyStdin routine pty stdin error")
|
||||||
|
break out
|
||||||
|
}
|
||||||
|
_, err := tc.ptmx.Write(buf)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to send data to pty stdin: ", err)
|
||||||
|
break out
|
||||||
|
}
|
||||||
|
case <-tc.ws_done:
|
||||||
|
log.Println("Exit wsToPtyStdin routine as ws is going away")
|
||||||
|
break out
|
||||||
|
case <-tc.pty_done:
|
||||||
|
log.Println("Exit wsToPtyStdin routine as pty is going away")
|
||||||
|
break out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("wsToPtyStdin routine exited")
|
||||||
|
}
|
||||||
|
|
||||||
|
// shovel data from pty Stdout to WS
|
||||||
|
func (tc *TermConn) ptyStdoutToWs(wg *sync.WaitGroup) {
|
||||||
|
var viewers []*websocket.Conn
|
||||||
|
|
||||||
|
defer wg.Done()
|
||||||
|
bufChan := make(chan []byte)
|
||||||
|
|
||||||
|
go func() { //create a goroutine to read from pty
|
||||||
|
for {
|
||||||
|
readBuf := make([]byte, 1024) //pty reads in 1024 blocks
|
||||||
|
n, err := tc.ptmx.Read(readBuf)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to read from pty stdout: ", err)
|
||||||
|
close(bufChan)
|
||||||
|
close(tc.pty_done)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
readBuf = readBuf[:n] // slice the buffer so that it is exact the size of data read.
|
||||||
|
bufChan <- readBuf
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
out:
|
||||||
|
for {
|
||||||
|
// handle viewers, we want to use non-blocking receive
|
||||||
|
select {
|
||||||
|
case buf, ok := <-bufChan:
|
||||||
|
if !ok {
|
||||||
|
tc.ws.SetWriteDeadline(time.Now().Add(writeWait))
|
||||||
|
tc.ws.WriteMessage(websocket.CloseMessage,
|
||||||
|
websocket.FormatCloseMessage(websocket.CloseNormalClosure, "Pty closed"))
|
||||||
|
|
||||||
|
break out
|
||||||
|
}
|
||||||
|
// We could add ws to viewers as well (then we can use io.MultiWriter),
|
||||||
|
// but we want to handle errors differently
|
||||||
|
tc.ws.SetWriteDeadline(time.Now().Add(writeWait))
|
||||||
|
if err := tc.ws.WriteMessage(websocket.BinaryMessage, buf); err != nil {
|
||||||
|
log.Println("Failed to write message: ", err)
|
||||||
|
break out
|
||||||
|
}
|
||||||
|
|
||||||
|
//write to the viewer
|
||||||
|
for i, w := range viewers {
|
||||||
|
if w == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the viewer exits, we will just ignore the error
|
||||||
|
w.SetWriteDeadline(time.Now().Add(viewWait))
|
||||||
|
if err := w.WriteMessage(websocket.BinaryMessage, buf); err != nil {
|
||||||
|
log.Println("Failed to write message to viewer: ", err)
|
||||||
|
|
||||||
|
viewers[i] = nil
|
||||||
|
w.Close() // we own the socket and need to close it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case viewer := <-tc.vchan:
|
||||||
|
log.Println("Received viewer")
|
||||||
|
viewers = append(viewers, viewer)
|
||||||
|
|
||||||
|
case <-tc.ws_done:
|
||||||
|
log.Println("Exit ptyStdoutToWs routine as ws is going away")
|
||||||
|
break out
|
||||||
|
|
||||||
|
case <-tc.pty_done:
|
||||||
|
log.Println("Exit ptyStdoutToWs routine as pty is going away")
|
||||||
|
break out // do not block on these two channels
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// close the watcher
|
||||||
|
for _, w := range viewers {
|
||||||
|
if w != nil {
|
||||||
|
w.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("ptyStdoutToWs routine exited")
|
||||||
|
}
|
||||||
|
|
||||||
|
// this function should be executed by the main goroutine for the connection
|
||||||
|
func (tc *TermConn) release() {
|
||||||
|
log.Println("Releasing terminal connection", tc.Name)
|
||||||
|
|
||||||
|
registry.removePlayer(tc.Name)
|
||||||
|
|
||||||
|
if tc.ptmx != nil {
|
||||||
|
// cleanup the pty and its related process
|
||||||
|
tc.ptmx.Close()
|
||||||
|
|
||||||
|
// terminate the command line process
|
||||||
|
proc := tc.cmd.Process
|
||||||
|
|
||||||
|
// send an interrupt, this will cause the shell process to
|
||||||
|
// return from syscalls if any is pending
|
||||||
|
if err := proc.Signal(os.Interrupt); err != nil {
|
||||||
|
log.Printf("Failed to send Interrupt to shell process(%v): %v ", proc.Pid, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for a second for shell process to interrupt before kill it
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
|
log.Printf("Try to kill the shell process(%v)", proc.Pid)
|
||||||
|
|
||||||
|
if err := proc.Signal(os.Kill); err != nil {
|
||||||
|
log.Printf("Failed to send KILL to shell process(%v): %v", proc.Pid, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := proc.Wait(); err != nil {
|
||||||
|
log.Printf("Failed to wait for shell process(%v): %v", proc.Pid, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
close(tc.vchan)
|
||||||
|
}
|
||||||
|
|
||||||
|
tc.ws.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle websockets
|
||||||
|
func handlePlayer(w http.ResponseWriter, r *http.Request, cmdline []string) {
|
||||||
|
ws, err := upgrader.Upgrade(w, r, nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to create websocket: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tc := TermConn{
|
||||||
|
ws: ws,
|
||||||
|
Name: uniuri.New(),
|
||||||
|
Ip: ws.RemoteAddr().String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
defer tc.release()
|
||||||
|
log.Println("\n\nCreated the websocket")
|
||||||
|
|
||||||
|
tc.ws_done = make(chan struct{})
|
||||||
|
tc.pty_done = make(chan struct{})
|
||||||
|
tc.vchan = make(chan *websocket.Conn)
|
||||||
|
|
||||||
|
if err := tc.createPty(cmdline); err != nil {
|
||||||
|
log.Println("Failed to create PTY: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.addPlayer(&tc)
|
||||||
|
|
||||||
|
// main event loop to shovel data between ws and pty
|
||||||
|
// do not call ptyStdoutToWs in this goroutine, otherwise
|
||||||
|
// the websocket will not close. This is because ptyStdoutToWs
|
||||||
|
// is usually blocked in the pty.Read
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(3)
|
||||||
|
|
||||||
|
go tc.ping(&wg)
|
||||||
|
go tc.ptyStdoutToWs(&wg)
|
||||||
|
go tc.wsToPtyStdin(&wg)
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
log.Println("Wait returned")
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle websockets
|
||||||
|
func handleViewer(w http.ResponseWriter, r *http.Request, path string) {
|
||||||
|
ws, err := upgrader.Upgrade(w, r, nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to create websocket: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("\n\nCreated the websocket")
|
||||||
|
if !registry.sendToPlayer(path, ws) {
|
||||||
|
log.Println("Failed to send websocket to player, close it")
|
||||||
|
ws.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConnectTerm(w http.ResponseWriter, r *http.Request, isViewer bool, path string, cmdline []string) {
|
||||||
|
if !isViewer {
|
||||||
|
handlePlayer(w, r, cmdline)
|
||||||
|
} else {
|
||||||
|
handleViewer(w, r, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Init(checkOrigin func(r *http.Request) bool) {
|
||||||
|
upgrader.CheckOrigin = checkOrigin
|
||||||
|
registry.init()
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user