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
3 changes: 3 additions & 0 deletions .gotty
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
// [bool] Permit clients to write to the TTY
// permit_write = false

// [bool] Log user's writes in the TTY
// write_log = false

// [bool] Enable basic authentication
// enable_basic_auth = false

Expand Down
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ By default, GoTTY starts a web server at port 8080. Open the URL on your web bro
--port value, -p value Port number to liten (default: "8080") [$GOTTY_PORT]
--path value, -m value Base path (default: "/") [$GOTTY_PATH]
--permit-write, -w Permit clients to write to the TTY (BE CAREFUL) (default: false) [$GOTTY_PERMIT_WRITE]
--write-log Log user's writes in the TTY (default: false) [$GOTTY_WRITE_LOG]
--credential value, -c value Credential for Basic Authentication (ex: user:pass, default disabled) [$GOTTY_CREDENTIAL]
--random-url, -r Add a random string to the URL (default: false) [$GOTTY_RANDOM_URL]
--random-url-length value Random URL length (default: 8) [$GOTTY_RANDOM_URL_LENGTH]
Expand Down Expand Up @@ -151,6 +152,36 @@ When you want to create a jailed environment for each client, you can use Docker
$ gotty -w docker run -it --rm busybox
```

## Write log

If you set `--write-log` option, user's writes in the TTY can be Logged. for example:

if you run gotty like this:

```shell
./gotty -w --write-log --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 user's writes in the log (operation logs):

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

Using the `[write-log]` flag, you can collect and store these logs persistently. All args are in the log, including the userID.

## Development

You can build a binary by simply running `make`. go1.16 is required.
Expand Down
4 changes: 4 additions & 0 deletions server/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,14 @@ 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())
}
if server.options.WriteLog {
opts = append(opts, webtty.WithWriteLog())
}
if server.options.EnableReconnect {
opts = append(opts, webtty.WithReconnect(server.options.ReconnectTime))
}
Expand Down
1 change: 1 addition & 0 deletions server/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ type Options struct {
Port string `hcl:"port" flagName:"port" flagSName:"p" flagDescribe:"Port number to liten" default:"8080"`
Path string `hcl:"path" flagName:"path" flagSName:"m" flagDescribe:"Base path" default:"/"`
PermitWrite bool `hcl:"permit_write" flagName:"permit-write" flagSName:"w" flagDescribe:"Permit clients to write to the TTY (BE CAREFUL)" default:"false"`
WriteLog bool `hcl:"write_log" flagName:"write-log" flagDescribe:"Log user's writes in the TTY" default:"false"`
EnableBasicAuth bool `hcl:"enable_basic_auth" default:"false"`
Credential string `hcl:"credential" flagName:"credential" flagSName:"c" flagDescribe:"Credential for Basic Authentication (ex: user:pass, default disabled)" default:""`
EnableRandomUrl bool `hcl:"enable_random_url" flagName:"random-url" flagSName:"r" flagDescribe:"Add a random string to the URL" default:"false"`
Expand Down
105 changes: 105 additions & 0 deletions utils/log.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package utils

import "fmt"

var CtrlChar = 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",
}

var CtrlCharGroup = map[string]string{
"1B5B41": "UP",
"1B5B42": "DOWN",
"1B5B43": "RIGHT",
"1B5B44": "LEFT",

// shell control codes
// codes[0]+codes[1]+codes[n-1]
// for example:
// [1B(ESC) 5B([) 32(2) 3B(;) 35(5) 52(R)]: row 2 col 5
// [1B(ESC) 5B([) 31(1) 30(0) 3B(;) 35(5) 52(R)]: row 10 col 5
// [1B(ESC) 5B([) 32(2) 32(2) 3B(;) 35(5) 52(R)]: row 22 col 5
"1B5B52": "",

// maybe there will be more control char group
// ...
}

func FormatWriteLog(codes []byte, line *string) {
n := len(codes)
// when user uses the keyboard arrow keys
// arrow keys are combination of 3 ASCII codes
if n == 3 {
llaoj marked this conversation as resolved.
Show resolved Hide resolved
if str, exist := ASCIIGroupToStr(fmt.Sprintf("%X", codes)); exist {
*line += str
return
}
}
// for some shells
// they will automatically send some control characters
// after typing the command and pressing Enter
// which indicate the current row and column.
if n >= 6 {
if str, exist := ASCIIGroupToStr(fmt.Sprintf("%X", []byte{codes[0], codes[1], codes[n-1]})); exist {
*line += str
return
}
}
llaoj marked this conversation as resolved.
Show resolved Hide resolved

str := ASCIIToStr(codes)
*line += str

return
}

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

return
}

func ASCIIGroupToStr(group string) (string, bool) {
if value, ok := CtrlCharGroup[group]; ok {
return value, true
}

return "", false
}
16 changes: 16 additions & 0 deletions webtty/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ func WithPermitWrite() Option {
}
}

// WithWriteLog sets a WebTTY to log user's writes in the TTY.
func WithWriteLog() Option {
return func(wt *WebTTY) error {
wt.writeLog = true
return nil
}
}

// WithFixedColumns sets a fixed width to TTY master.
func WithFixedColumns(columns int) Option {
return func(wt *WebTTY) error {
Expand All @@ -41,6 +49,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
23 changes: 21 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,7 +21,9 @@ type WebTTY struct {
slave Slave

windowTitle []byte
arguments map[string][]string
permitWrite bool
writeLog bool
columns int
rows int
reconnect int // in seconds
Expand Down Expand Up @@ -93,13 +97,18 @@ func (wt *WebTTY) Run(ctx context.Context) error {
go func() {
errs <- func() error {
buffer := make([]byte, wt.bufferSize)
var line string
arguments, err := json.Marshal(wt.arguments)
if err != nil {
return err
}
for {
n, err := wt.masterConn.Read(buffer)
if err != nil {
return ErrMasterClosed
}

err = wt.handleMasterReadEvent(buffer[:n])
err = wt.handleMasterReadEvent(buffer[:n], &line, arguments)
if err != nil {
return err
}
Expand Down Expand Up @@ -168,7 +177,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, argument []byte) error {
if len(data) == 0 {
return errors.New("unexpected zero length read from master")
}
Expand All @@ -189,6 +198,16 @@ func (wt *WebTTY) handleMasterReadEvent(data []byte) error {
return errors.Wrapf(err, "failed to decode received data")
}

if wt.writeLog {
utils.FormatWriteLog(decodedBuffer[:n], line)
// 13(ASCII) means carriage return(CR)
// it is the end of a line
if decodedBuffer[n-1] == 13 {
log.Printf("[write-log] %s %s\n", argument, *line)
*line = ""
}
}

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