Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Log user's writes in the TTY #57

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,36 @@ When you want to create a jailed environment for each client, you can use Docker
$ gotty -w docker run -it --rm busybox
```

## Writes log

User's input in terminal can be found in logs. for example:

if you run gotty like this:

```shell
./gotty -w --permit-arguments ./test.sh
```

this is `test.sh`:

```sh
#!/bin/bash

echo "Welcome: $4"
kubectl -n $1 exec -it $2 -c $3 -- sh
```

visit `http://127.0.0.1:8080/?arg=without-istio&arg=sleep-7b6d569576-57sjq&arg=sleep&arg=21001713` and input your commands in shell, and you will see operation logs in stdout:

```
...
2022/11/13 10:48:12 [wlog] lsCR {"arg":["without-istio","sleep-7b6d569576-57sjq","sleep","21001713"]}
2022/11/13 10:48:14 [wlog] pwdCR {"arg":["without-istio","sleep-7b6d569576-57sjq","sleep","21001713"]}
...
```

Using the `[wlog]` flag, you can collect and store these logs persistently. All args are in the log, including the userID.
llaoj marked this conversation as resolved.
Show resolved Hide resolved

## Development

You can build a binary by simply running `make`. go1.16 is required.
Expand Down
1 change: 1 addition & 0 deletions server/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ func (server *Server) processWSConn(ctx context.Context, conn *websocket.Conn, h

opts := []webtty.Option{
webtty.WithWindowTitle(titleBuf.Bytes()),
webtty.WithArguments(params),
}
if server.options.PermitWrite {
opts = append(opts, webtty.WithPermitWrite())
Expand Down
96 changes: 96 additions & 0 deletions utils/log.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package utils

import "fmt"

func FormatWritesLog(codes []byte, line *string) {
llaoj marked this conversation as resolved.
Show resolved Hide resolved
n := len(codes)
str := ""
exist := false
if n == 3 {
llaoj marked this conversation as resolved.
Show resolved Hide resolved
if str, exist = ASCIIGroupToStr(fmt.Sprintf("%X", codes)); exist {
*line += str
codes = nil
}
}
// sh control codes
if n >= 6 {
if str, exist = ASCIIGroupToStr(fmt.Sprintf("%X", []byte{codes[0], codes[1], codes[n-1]})); exist {
*line += str
codes = nil
}
}
llaoj marked this conversation as resolved.
Show resolved Hide resolved

if codes != nil {
str = ASCIIToStr(codes)
*line += str
}

return
}

func ASCIIToStr(codes []byte) string {
control := map[byte]string{
0: "NUL",
1: "SOH",
2: "STX",
3: "ETX",
4: "EOT",
5: "ENQ",
6: "ACK",
7: "BEL",
8: "BS",
9: "HT",
10: "LF",
11: "VT",
12: "FF",
13: "CR",
14: "SO",
15: "SI",
16: "DLE",
17: "DCI",
18: "DC2",
19: "DC3",
20: "DC4",
21: "NAK",
22: "SYN",
23: "TB",
24: "CAN",
25: "EM",
26: "SUB",
27: "ESC",
28: "FS",
29: "GS",
30: "RS",
31: "US",
32: "SPACE",
127: "DEL",
}
llaoj marked this conversation as resolved.
Show resolved Hide resolved

str := ""
for _, code := range codes {
if value, ok := control[code]; ok {
str += value
} else {
str += string(code)
}
}
llaoj marked this conversation as resolved.
Show resolved Hide resolved

return str
}

func ASCIIGroupToStr(sum string) (string, bool) {
group := map[string]string{
"1B5B41": "UP",
"1B5B42": "DOWN",
"1B5B43": "RIGHT",
"1B5B44": "LEFT",
// sh control codes: codes[0]codes[1]codes[5]
// eg. "ESC[ 1;5 R"
"1B5B52": "",
}
llaoj marked this conversation as resolved.
Show resolved Hide resolved
if value, ok := group[sum]; ok {
return value, true
}

return "", false
}
8 changes: 8 additions & 0 deletions webtty/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ func WithWindowTitle(windowTitle []byte) Option {
}
}

// WithArguments sets the command line arguments that clients send
func WithArguments(arguments map[string][]string) Option {
return func(wt *WebTTY) error {
wt.arguments = arguments
return nil
}
}

// WithReconnect enables reconnection on the master side.
func WithReconnect(timeInSeconds int) Option {
return func(wt *WebTTY) error {
Expand Down
19 changes: 17 additions & 2 deletions webtty/webtty.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"context"
"encoding/base64"
"encoding/json"
"github.com/sorenisanerd/gotty/utils"
"log"
"sync"

"github.com/pkg/errors"
Expand All @@ -19,6 +21,7 @@ type WebTTY struct {
slave Slave

windowTitle []byte
arguments map[string][]string
permitWrite bool
columns int
rows int
Expand Down Expand Up @@ -88,13 +91,14 @@ func (wt *WebTTY) Run(ctx context.Context) error {
go func() {
errs <- func() error {
buffer := make([]byte, wt.bufferSize)
line := ""
llaoj marked this conversation as resolved.
Show resolved Hide resolved
for {
n, err := wt.masterConn.Read(buffer)
if err != nil {
return ErrMasterClosed
}

err = wt.handleMasterReadEvent(buffer[:n])
err = wt.handleMasterReadEvent(buffer[:n], &line)
if err != nil {
return err
}
Expand Down Expand Up @@ -163,7 +167,7 @@ func (wt *WebTTY) masterWrite(data []byte) error {
return nil
}

func (wt *WebTTY) handleMasterReadEvent(data []byte) error {
func (wt *WebTTY) handleMasterReadEvent(data []byte, line *string) error {
if len(data) == 0 {
return errors.New("unexpected zero length read from master")
}
Expand All @@ -184,6 +188,17 @@ func (wt *WebTTY) handleMasterReadEvent(data []byte) error {
return errors.Wrapf(err, "failed to decode received data")
}

// log.Printf("[wlog] %v %X\n", decodedBuffer[:n], decodedBuffer[:n])
llaoj marked this conversation as resolved.
Show resolved Hide resolved
utils.FormatWritesLog(decodedBuffer[:n], line)
if decodedBuffer[n-1] == 13 {
llaoj marked this conversation as resolved.
Show resolved Hide resolved
argumentsByte, err := json.Marshal(wt.arguments)
llaoj marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return errors.Wrapf(err, "failed to marshal arguments map")
}
log.Printf("[wlog] %s %s\n", *line, string(argumentsByte))
*line = ""
}

_, err = wt.slave.Write(decodedBuffer[:n])
if err != nil {
return errors.Wrapf(err, "failed to write received data to slave")
Expand Down