mirror of
https://github.com/syssecfsu/witty.git
synced 2025-01-26 20:14:32 +01:00
Now with viewers
This commit is contained in:
parent
2720c1ba3f
commit
9541171129
66
README.md
66
README.md
@ -1,44 +1,52 @@
|
||||
# Web Terminal
|
||||
A (unsafe) technical demo to export a shell to web browser.
|
||||
It is just a simple demo in case some people are interested in
|
||||
how to setup xterm.js with websocket.
|
||||
# Web-based Terminal Emulator
|
||||
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">
|
||||
|
||||
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
|
||||
Gin web framework, gorilla/websocket, pty, and xterm.js!
|
||||
The workflow is simple, the client will initiate a terminal
|
||||
window (xterm.js) and create a websocket with the server. On
|
||||
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```.
|
||||
window (xterm.js) and create a websocket with the server, which relays the data between pty and xterm.
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
The program
|
||||
has been tested on Linux, WSL2, Raspberry Pi 3B (Debian), and MacOSX.
|
||||
1. Install the [go](https://go.dev/) compiler.
|
||||
2. Download the release and unzip it, or clone the repo
|
||||
|
||||
```git clone https://github.com/syssecfsu/web_terminal.git```
|
||||
|
||||
***known bug***
|
||||
3. Go to the ```tls``` directory and create a self-signed cert
|
||||
|
||||
\# Generate a private key for a curve
|
||||
|
||||
On MacOS X, running zsh with web_terminal will produce an extra %
|
||||
each time in Google Chrome. Consider it a ___feautre___, will not
|
||||
fix unless there is a pull request. Safari works fine though.
|
||||
```openssl ecparam -name prime256v1 -genkey -noout -out private-key.pem```
|
||||
|
||||
\# Create a self-signed certificate
|
||||
|
||||
**NOTE**
|
||||
```openssl req -new -x509 -key private-key.pem -out cert.pem -days 360```
|
||||
|
||||
___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.___
|
||||
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
|
||||
[pi-hole](https://pi-hole.net/)
|
||||
|
@ -21,7 +21,7 @@
|
||||
<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" role="button">New Session</a>
|
||||
<a class="btn btn-primary btn-sm float-end" href="/new" target="_blank" role="button">New Session</a>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
@ -31,13 +31,17 @@
|
||||
<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 {{.ip}}, running {{.cmd}}</p>
|
||||
<a class="btn btn-secondary btn-sm float-end" href="{{.url}}" role="button">View</a>
|
||||
<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>
|
||||
|
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 (
|
||||
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-gonic/gin v1.7.7 // 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/golang/protobuf v1.3.3 // 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/leodido/go-urn v1.2.0 // 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/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/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/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
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/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
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/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||
|
42
main.go
42
main.go
@ -31,6 +31,30 @@ func checkOrigin(r *http.Request) bool {
|
||||
return (host != nil) && (*host == h.Host)
|
||||
}
|
||||
|
||||
type InteractiveSession struct {
|
||||
Ip string
|
||||
Cmd string
|
||||
Name string
|
||||
}
|
||||
|
||||
func fillIndex(c *gin.Context) {
|
||||
var players []InteractiveSession
|
||||
|
||||
term_conn.ForEachSession(func(tc *term_conn.TermConn) {
|
||||
players = append(players, InteractiveSession{
|
||||
Name: tc.Name,
|
||||
Ip: tc.Ip,
|
||||
Cmd: cmdToExec[0],
|
||||
})
|
||||
})
|
||||
|
||||
c.HTML(http.StatusOK, "index.html", gin.H{
|
||||
"title": "Interactive terminal",
|
||||
"path": "/ws_do",
|
||||
"players": players,
|
||||
})
|
||||
}
|
||||
|
||||
func main() {
|
||||
fp, err := os.OpenFile("web_term.log", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
|
||||
@ -56,10 +80,11 @@ func main() {
|
||||
rt.SetTrustedProxies(nil)
|
||||
rt.LoadHTMLGlob("./assets/*.html")
|
||||
|
||||
rt.GET("/view/*sname", func(c *gin.Context) {
|
||||
rt.GET("/view/:sname", func(c *gin.Context) {
|
||||
sname := c.Param("sname")
|
||||
c.HTML(http.StatusOK, "term.html", gin.H{
|
||||
"title": "Viewer terminal",
|
||||
"path": "/ws_view",
|
||||
"path": "/ws_view/" + sname,
|
||||
})
|
||||
})
|
||||
|
||||
@ -75,11 +100,12 @@ func main() {
|
||||
})
|
||||
|
||||
rt.GET("/ws_do", func(c *gin.Context) {
|
||||
term_conn.ConnectTerm(c.Writer, c.Request, false, cmdToExec)
|
||||
term_conn.ConnectTerm(c.Writer, c.Request, false, "", cmdToExec)
|
||||
})
|
||||
|
||||
rt.GET("/ws_view", func(c *gin.Context) {
|
||||
term_conn.ConnectTerm(c.Writer, c.Request, true, nil)
|
||||
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
|
||||
@ -87,11 +113,7 @@ func main() {
|
||||
|
||||
rt.GET("/", func(c *gin.Context) {
|
||||
host = &c.Request.Host
|
||||
|
||||
c.HTML(http.StatusOK, "index.html", gin.H{
|
||||
"title": "Interactive terminal",
|
||||
"path": "/ws_do",
|
||||
})
|
||||
fillIndex(c)
|
||||
})
|
||||
|
||||
term_conn.Init(checkOrigin)
|
||||
|
@ -21,12 +21,13 @@ func (reg *Registry) init() {
|
||||
reg.players = make(map[string]*TermConn)
|
||||
}
|
||||
|
||||
func (d *Registry) addPlayer(name string, tc *TermConn) {
|
||||
func (d *Registry) addPlayer(tc *TermConn) {
|
||||
d.mtx.Lock()
|
||||
if _, ok := d.players[name]; ok {
|
||||
log.Println(name, "already exist in the dispatcher, skip registration")
|
||||
if _, ok := d.players[tc.Name]; ok {
|
||||
log.Println(tc.Name, "Already exist in the dispatcher, skip registration")
|
||||
} else {
|
||||
d.players[name] = tc
|
||||
d.players[tc.Name] = tc
|
||||
log.Println("Add interactive session to registry", tc.Name)
|
||||
}
|
||||
d.mtx.Unlock()
|
||||
}
|
||||
@ -38,6 +39,7 @@ func (d *Registry) removePlayer(name string) error {
|
||||
if _, ok := d.players[name]; ok {
|
||||
delete(d.players, name)
|
||||
err = nil
|
||||
log.Println("Removed interactive session to registry", name)
|
||||
}
|
||||
|
||||
d.mtx.Unlock()
|
||||
@ -56,3 +58,11 @@ func (d *Registry) sendToPlayer(name string, ws *websocket.Conn) bool {
|
||||
d.mtx.Unlock()
|
||||
return ok
|
||||
}
|
||||
|
||||
func ForEachSession(fp func(tc *TermConn)) {
|
||||
registry.mtx.Lock()
|
||||
for _, v := range registry.players {
|
||||
fp(v)
|
||||
}
|
||||
registry.mtx.Unlock()
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/creack/pty"
|
||||
"github.com/dchest/uniuri"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
@ -29,9 +30,6 @@ const (
|
||||
|
||||
// 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
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
@ -45,10 +43,11 @@ var upgrader = websocket.Upgrader{
|
||||
// TermConn represents the connected websocket and pty.
|
||||
// if isViewer is true
|
||||
type TermConn struct {
|
||||
ws *websocket.Conn
|
||||
name string
|
||||
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
|
||||
@ -259,9 +258,9 @@ out:
|
||||
|
||||
// this function should be executed by the main goroutine for the connection
|
||||
func (tc *TermConn) release() {
|
||||
log.Println("Releasing terminal connection", tc.name)
|
||||
log.Println("Releasing terminal connection", tc.Name)
|
||||
|
||||
registry.removePlayer(tc.name)
|
||||
registry.removePlayer(tc.Name)
|
||||
|
||||
if tc.ptmx != nil {
|
||||
// cleanup the pty and its related process
|
||||
@ -306,7 +305,8 @@ func handlePlayer(w http.ResponseWriter, r *http.Request, cmdline []string) {
|
||||
|
||||
tc := TermConn{
|
||||
ws: ws,
|
||||
name: "main",
|
||||
Name: uniuri.New(),
|
||||
Ip: ws.RemoteAddr().String(),
|
||||
}
|
||||
|
||||
defer tc.release()
|
||||
@ -321,7 +321,7 @@ func handlePlayer(w http.ResponseWriter, r *http.Request, cmdline []string) {
|
||||
return
|
||||
}
|
||||
|
||||
registry.addPlayer("main", &tc)
|
||||
registry.addPlayer(&tc)
|
||||
|
||||
// main event loop to shovel data between ws and pty
|
||||
// do not call ptyStdoutToWs in this goroutine, otherwise
|
||||
@ -339,7 +339,7 @@ func handlePlayer(w http.ResponseWriter, r *http.Request, cmdline []string) {
|
||||
}
|
||||
|
||||
// handle websockets
|
||||
func handleViewer(w http.ResponseWriter, r *http.Request) {
|
||||
func handleViewer(w http.ResponseWriter, r *http.Request, path string) {
|
||||
ws, err := upgrader.Upgrade(w, r, nil)
|
||||
|
||||
if err != nil {
|
||||
@ -348,17 +348,17 @@ func handleViewer(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
log.Println("\n\nCreated the websocket")
|
||||
if !registry.sendToPlayer("main", ws) {
|
||||
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, cmdline []string) {
|
||||
func ConnectTerm(w http.ResponseWriter, r *http.Request, isViewer bool, path string, cmdline []string) {
|
||||
if !isViewer {
|
||||
handlePlayer(w, r, cmdline)
|
||||
} else {
|
||||
handleViewer(w, r)
|
||||
handleViewer(w, r, path)
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user