Merge pull request #1 from syssecfsu/observer

Observer
This commit is contained in:
syssecfsu 2022-01-11 21:19:56 -05:00 committed by GitHub
commit dd750d0c19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1035 additions and 290 deletions

View File

@ -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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

1
assets/external/bootstrap.min.js.map vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -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">
<a class="navbar-brand">
<img src="/assets/logo.svg" style="margin-left: 2em;margin-right: 1em;" height="24"
class="d-inline-block align-text-top">
Web-based Terminal Emulator
<a class="btn btn-primary btn-sm float-end" href="/new" target="_blank" role="button">New Session</a>
</div> </div>
<script> </nav>
term = createTerminal(); </header>
// 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); <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
View 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

View File

@ -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;
}

View File

@ -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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 KiB

0
extra/screencast.gif Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 955 KiB

After

Width:  |  Height:  |  Size: 955 KiB

2
go.mod
View File

@ -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
View File

@ -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=

267
main.go
View File

@ -1,66 +1,22 @@
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,
CheckOrigin: func(r *http.Request) bool {
org := r.Header.Get("Origin") org := r.Header.Get("Origin")
h, err := url.Parse(org) h, err := url.Parse(org)
@ -73,163 +29,30 @@ var upgrader = websocket.Upgrader{
} }
return (host != nil) && (*host == h.Host) return (host != nil) && (*host == h.Host)
},
} }
// Periodically send ping message to detect the status of the ws type InteractiveSession struct {
func ping(ws *websocket.Conn, done chan struct{}) { Ip string
ticker := time.NewTicker(pingPeriod) Cmd string
defer ticker.Stop() Name string
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
}
}
} }
// shovel data from websocket to pty stdin func fillIndex(c *gin.Context) {
func toPtyStdin(ws *websocket.Conn, ptmx *os.File) { var players []InteractiveSession
ws.SetReadLimit(maxMessageSize)
// set the readdeadline. The idea here is simple, term_conn.ForEachSession(func(tc *term_conn.TermConn) {
// as long as we keep receiving pong message, players = append(players, InteractiveSession{
// the readdeadline will keep updating. Otherwise Name: tc.Name,
// read will timeout. Ip: tc.Ip,
ws.SetReadDeadline(time.Now().Add(pongWait)) Cmd: cmdToExec[0],
ws.SetPongHandler(func(string) error { })
ws.SetReadDeadline(time.Now().Add(pongWait))
return nil
}) })
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
View 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
View 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()
}