Skip to content

Commit

Permalink
add asciicast v2 log format support and change tty log filename from …
Browse files Browse the repository at this point in the history
…username to uniqueID
  • Loading branch information
clysto committed Oct 17, 2024
1 parent 5ef0d03 commit 23a44da
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 2 deletions.
125 changes: 125 additions & 0 deletions cmd/sshpiperd/asciicast.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package main

import (
"bytes"
"encoding/binary"
"encoding/json"
"fmt"
"os"
"path"
"time"
)

const (
msgChannelRequest = 98
)

func jsonEscape(i string) string {
b, err := json.Marshal(i)
if err != nil {
panic(err)
}
s := string(b)
return s[1 : len(s)-1]
}

func readString(buf *bytes.Reader) string {
var l uint32
binary.Read(buf, binary.BigEndian, &l)

Check failure on line 28 in cmd/sshpiperd/asciicast.go

View workflow job for this annotation

GitHub Actions / Build

Error return value of `binary.Read` is not checked (errcheck)
s := make([]byte, l)
buf.Read(s)

Check failure on line 30 in cmd/sshpiperd/asciicast.go

View workflow job for this annotation

GitHub Actions / Build

Error return value of `buf.Read` is not checked (errcheck)
return string(s)
}

type asciicastLogger struct {
cast *os.File
starttime time.Time
envs map[string]string
initWidth uint32
initHeight uint32
}

func newAsciicastLogger(logdir string) (*asciicastLogger, error) {
f, err := os.OpenFile(path.Join(logdir, "shell.cast"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)

if err != nil {
return nil, err
}

return &asciicastLogger{
cast: f,
starttime: time.Now(),
envs: make(map[string]string),
}, nil
}

func (l *asciicastLogger) uphook(msg []byte) ([]byte, error) {
if msg[0] == msgChannelData {
buf := msg[9:]
t := time.Since(l.starttime).Seconds()

_, err := fmt.Fprintf(l.cast, "[%v,\"o\",\"%s\"]\n", t, jsonEscape(string(buf)))

if err != nil {
return msg, err
}

}
return msg, nil
}

func (l *asciicastLogger) downhook(msg []byte) ([]byte, error) {
if msg[0] == msgChannelRequest {
t := time.Since(l.starttime).Seconds()
buf := bytes.NewReader(msg[5:])
reqType := readString(buf)

switch reqType {
case "pty-req":
buf.ReadByte()

Check failure on line 79 in cmd/sshpiperd/asciicast.go

View workflow job for this annotation

GitHub Actions / Build

Error return value of `buf.ReadByte` is not checked (errcheck)
term := readString(buf)
binary.Read(buf, binary.BigEndian, &l.initWidth)

Check failure on line 81 in cmd/sshpiperd/asciicast.go

View workflow job for this annotation

GitHub Actions / Build

Error return value of `binary.Read` is not checked (errcheck)
binary.Read(buf, binary.BigEndian, &l.initHeight)

Check failure on line 82 in cmd/sshpiperd/asciicast.go

View workflow job for this annotation

GitHub Actions / Build

Error return value of `binary.Read` is not checked (errcheck)
l.envs["TERM"] = term
case "env":
buf.ReadByte()

Check failure on line 85 in cmd/sshpiperd/asciicast.go

View workflow job for this annotation

GitHub Actions / Build

Error return value of `buf.ReadByte` is not checked (errcheck)
varName := readString(buf)
varValue := readString(buf)
l.envs[varName] = varValue
case "window-change":
buf.ReadByte()

Check failure on line 90 in cmd/sshpiperd/asciicast.go

View workflow job for this annotation

GitHub Actions / Build

Error return value of `buf.ReadByte` is not checked (errcheck)
var width, height uint32
binary.Read(buf, binary.BigEndian, &width)
binary.Read(buf, binary.BigEndian, &height)
_, err := fmt.Fprintf(l.cast, "[%v,\"r\", \"%vx%v\"]\n", t, width, height)
if err != nil {
return msg, err
}
case "shell", "exec":
jsonEnvs, err := json.Marshal(l.envs)

if err != nil {
return msg, err
}

_, err = fmt.Fprintf(
l.cast,
"{\"version\": 2, \"width\": %d, \"height\": %d, \"timestamp\": %d, \"env\": %v}\n",
l.initWidth,
l.initHeight,
l.starttime.Unix(),
string(jsonEnvs),
)

if err != nil {
return msg, err
}
}
}
return msg, nil
}

func (l *asciicastLogger) Close() (err error) {
l.cast.Close()
return nil
}
18 changes: 16 additions & 2 deletions cmd/sshpiperd/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type daemon struct {
loginGraceTime time.Duration

recorddir string
asciicastdir string
filterHostkeysReqeust bool
}

Expand Down Expand Up @@ -217,19 +218,32 @@ func (d *daemon) run() error {
var uphook func([]byte) ([]byte, error)
var downhook func([]byte) ([]byte, error)

if d.recorddir != "" {
recorddir := path.Join(d.recorddir, p.DownstreamConnMeta().User())
uniqID := plugin.GetUniqueID(p.ChallengeContext())
if d.asciicastdir != "" {
recorddir := path.Join(d.asciicastdir, uniqID)
err = os.MkdirAll(recorddir, 0700)
if err != nil {
log.Errorf("cannot create screen recording dir %v: %v", recorddir, err)
return
}
recorder, err := newAsciicastLogger(recorddir)
if err != nil {
log.Errorf("cannot create screen recording logger: %v", err)
return
}
defer recorder.Close()

uphook = recorder.uphook
downhook = recorder.downhook
} else if d.recorddir != "" {
recorddir := path.Join(d.recorddir, uniqID)
err = os.MkdirAll(recorddir, 0700)
recorder, err := newFilePtyLogger(recorddir)
if err != nil {
log.Errorf("cannot create screen recording logger: %v", err)
return
}
defer recorder.Close()

uphook = recorder.loggingTty
}
Expand Down
10 changes: 10 additions & 0 deletions cmd/sshpiperd/internal/plugin/grpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -592,3 +592,13 @@ func DialCmd(cmd *exec.Cmd) (*CmdPlugin, error) {

return &CmdPlugin{*g, ch}, nil
}

func GetUniqueID(ctx ssh.ChallengeContext) string {
switch meta := ctx.(type) {
case *connMeta:
return meta.UniqId
case *chainConnMeta:
return meta.UniqId
}
panic("unknown challenge context")
}
7 changes: 7 additions & 0 deletions cmd/sshpiperd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,12 @@ func main() {
Usage: "create typescript format screen recording and save into the directory see https://linux.die.net/man/1/script",
EnvVars: []string{"SSHPIPERD_TYPESCRIPT_LOG_DIR"},
},
&cli.StringFlag{
Name: "asciicast-log-dir",
Value: "",
Usage: "create asciicast v2 format screen recording and save into the directory see https://docs.asciinema.org/manual/asciicast/v2",
EnvVars: []string{"SSHPIPERD_ASCIICAST_LOG_DIR"},
},
&cli.StringFlag{
Name: "banner-text",
Value: "",
Expand Down Expand Up @@ -257,6 +263,7 @@ func main() {
}

d.recorddir = ctx.String("typescript-log-dir")
d.asciicastdir = ctx.String("asciicast-log-dir")
d.filterHostkeysReqeust = ctx.Bool("drop-hostkeys-message")

go func() {
Expand Down

0 comments on commit 23a44da

Please sign in to comment.