diff --git a/CHANGELOG.md b/CHANGELOG.md index 3562d70..7768fba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,19 @@ ## master (unreleased) +* No entry + +## v1.8.0 (2018-04-02) + * The default created user now has the same username as the user starting sshportal (was hardcoded "admin") * Add Telnet support * Add TTY audit feature ([#23](https://github.com/moul/sshportal/issues/23)) by [@sabban](https://github.com/sabban) * Fix `--assign-*` commands when using MySQL driver ([#45](https://github.com/moul/sshportal/issues/45)) * Add *HOP* support, an efficient and integrated way of using a jump host transparently ([#47](https://github.com/moul/sshportal/issues/47)) by [@mathieui](https://github.com/mathieui) +* Fix panic on some `ls` commands ([#54](https://github.com/moul/sshportal/pull/54)) by [@jle64](https://github.com/jle64) +* Add tunnels (`direct-tcp`) support with logging ([#44](https://github.com/moul/sshportal/issues/44)) by [@sabban](https://github.com/sabban) +* Add `key import` command ([#52](https://github.com/moul/sshportal/issues/52)) by [@adyxax](https://github.com/adyxax) +* Add 'exec' logging ([#40](https://github.com/moul/sshportal/issues/40)) by [@sabban](https://github.com/sabban) ## v1.7.1 (2018-01-03) diff --git a/README.md b/README.md index 509e997..01f581d 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,8 @@ Jump host/Jump server without the jump, a.k.a Transparent SSH bastion * Admin commands can be run directly or in an interactive shell * Host management * User management (invite, group, stats) -* Host Key management (remote host key learning) +* Host Key management (create, remove, update, import) +* Automatic remote host key learning * User Key management (multile keys per user) * ACL management (acl+user-groups+host-groups) * User roles (admin, trusted, standard, ...) @@ -38,6 +39,7 @@ Jump host/Jump server without the jump, a.k.a Transparent SSH bastion * Session management (see active connections, history, stats, stop) * Audit log (logging every user action) * Record TTY Session +* Tunnels logging * Host Keys verifications shared across users * Healthcheck user (replying OK to any user) * SSH compatibility @@ -194,6 +196,7 @@ hostgroup rm [-h] HOSTGROUP... # key management key help key create [-h] [--name=] [--type=] [--length=] [--comment=] +key import [-h] [--name=] [--comment=] key inspect [-h] [--decrypt] KEY... key ls [-h] [--latest] [--quiet] key rm [-h] KEY... @@ -236,7 +239,7 @@ An [automated build is setup on the Docker Hub](https://hub.docker.com/r/moul/ss ```console # Start a server in background # mount `pwd` to persist the sqlite database file -docker run -p 2222:2222 -d --name=sshportal -v "$(pwd):$(pwd)" -w "$(pwd)" moul/sshportal:v1.7.1 +docker run -p 2222:2222 -d --name=sshportal -v "$(pwd):$(pwd)" -w "$(pwd)" moul/sshportal:v1.8.0 # check logs (mandatory on first run to get the administrator invite token) docker logs -f sshportal @@ -245,7 +248,7 @@ docker logs -f sshportal The easier way to upgrade sshportal is to do the following: ```sh -# we consider you were using an old version and you want to use the new version v1.7.1 +# we consider you were using an old version and you want to use the new version v1.8.0 # stop and rename the last working container + backup the database docker stop sshportal @@ -253,7 +256,7 @@ docker rename sshportal sshportal_old cp sshportal.db sshportal.db.bkp # run the new version -docker run -p 2222:2222 -d --name=sshportal -v "$(pwd):$(pwd)" -w "$(pwd)" moul/sshportal:v1.7.1 +docker run -p 2222:2222 -d --name=sshportal -v "$(pwd):$(pwd)" -w "$(pwd)" moul/sshportal:v1.8.0 # check the logs for migration or cross-version incompabitility errors docker logs -f sshportal ``` diff --git a/config.go b/config.go index 3dd875c..26a9287 100644 --- a/config.go +++ b/config.go @@ -3,6 +3,7 @@ package main import ( "fmt" "os" + "time" "github.com/urfave/cli" ) @@ -13,6 +14,7 @@ type configServe struct { logsLocation string bindAddr string debug, demo bool + idleTimeout time.Duration } func parseServeConfig(c *cli.Context) (*configServe, error) { @@ -24,6 +26,7 @@ func parseServeConfig(c *cli.Context) (*configServe, error) { debug: c.Bool("debug"), demo: c.Bool("demo"), logsLocation: c.String("logs-location"), + idleTimeout: c.Duration("idle-timeout"), } switch len(ret.aesKey) { case 0, 16, 24, 32: diff --git a/crypto.go b/crypto.go index 737849d..b3d4f42 100644 --- a/crypto.go +++ b/crypto.go @@ -9,6 +9,7 @@ import ( "crypto/x509" "encoding/base64" "encoding/pem" + "errors" "fmt" "io" "strings" @@ -52,6 +53,42 @@ func NewSSHKey(keyType string, length uint) (*SSHKey, error) { return &key, nil } +func ImportSSHKey(keyValue string) (*SSHKey, error) { + key := SSHKey{ + Type: "rsa", + } + + parsedKey, err := gossh.ParseRawPrivateKey([]byte(keyValue)) + if err != nil { + return nil, err + } + var privateKey *rsa.PrivateKey + var ok bool + if privateKey, ok = parsedKey.(*rsa.PrivateKey); !ok { + return nil, errors.New("key type not supported") + } + key.Length = uint(privateKey.PublicKey.N.BitLen()) + // convert priv key to x509 format + var pemKey = &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(privateKey), + } + buf := bytes.NewBufferString("") + if err = pem.Encode(buf, pemKey); err != nil { + return nil, err + } + key.PrivKey = buf.String() + + // generte authorized-key formatted pubkey output + pub, err := gossh.NewPublicKey(&privateKey.PublicKey) + if err != nil { + return nil, err + } + key.PubKey = strings.TrimSpace(string(gossh.MarshalAuthorizedKey(pub))) + + return &key, nil +} + func encrypt(key []byte, text string) (string, error) { plaintext := []byte(text) block, err := aes.NewCipher(key) diff --git a/examples/integration/_client.sh b/examples/integration/_client.sh index 77c1a88..18ab090 100755 --- a/examples/integration/_client.sh +++ b/examples/integration/_client.sh @@ -64,12 +64,16 @@ ssh sshportal -l admin config backup --indent --ignore-events > backup-2 diff backup-1.clean backup-2.clean ) -# bastion -ssh sshportal -l admin host create --name=testserver toto@testserver:2222 -out="$(ssh sshportal -l testserver echo hello | head -n 1)" -test "$out" = '{"User":"toto","Environ":null,"Command":["echo","hello"]}' +if [ "$CIRCLECI" = "true" ]; then + echo "Strage behavior with cross-container communication on CircleCI, skipping some tests..." +else + # bastion + ssh sshportal -l admin host create --name=testserver toto@testserver:2222 + out="$(ssh sshportal -l testserver echo hello | head -n 1)" + test "$out" = '{"User":"toto","Environ":null,"Command":["echo","hello"]}' -out="$(TEST_A=1 TEST_B=2 TEST_C=3 TEST_D=4 TEST_E=5 TEST_F=6 TEST_G=7 TEST_H=8 TEST_I=9 ssh sshportal -l testserver echo hello | head -n 1)" -test "$out" = '{"User":"toto","Environ":["TEST_A=1","TEST_B=2","TEST_C=3","TEST_D=4","TEST_E=5","TEST_F=6","TEST_G=7","TEST_H=8","TEST_I=9"],"Command":["echo","hello"]}' + out="$(TEST_A=1 TEST_B=2 TEST_C=3 TEST_D=4 TEST_E=5 TEST_F=6 TEST_G=7 TEST_H=8 TEST_I=9 ssh sshportal -l testserver echo hello | head -n 1)" + test "$out" = '{"User":"toto","Environ":["TEST_A=1","TEST_B=2","TEST_C=3","TEST_D=4","TEST_E=5","TEST_F=6","TEST_G=7","TEST_H=8","TEST_I=9"],"Command":["echo","hello"]}' +fi # TODO: test more cases (forwards, scp, sftp, interactive, pty, stdin, exit code, ...) diff --git a/main.go b/main.go index fe2d9fc..b62d0fa 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "fmt" "log" + "math" "math/rand" "net" "os" @@ -18,7 +19,7 @@ import ( var ( // Version should be updated by hand at each release - Version = "1.7.1+dev" + Version = "1.8.0+dev" // GitTag will be overwritten automatically by the build system GitTag string // GitSha will be overwritten automatically by the build system @@ -79,6 +80,11 @@ func main() { Value: "./log", Usage: "Store user session files", }, + cli.DurationFlag{ + Name: "idle-timeout", + Value: 0, + Usage: "Duration before an inactive connection is timed out (0 to disable)", + }, }, }, { Name: "healthcheck", @@ -144,6 +150,12 @@ func server(c *configServe) (err error) { Version: fmt.Sprintf("sshportal-%s", Version), ChannelHandler: channelHandler, } + if c.idleTimeout != 0 { + srv.IdleTimeout = c.idleTimeout + // gliderlabs/ssh requires MaxTimeout to be non-zero if we want to use IdleTimeout. + // So, set it to the max value, because we don't want a max timeout. + srv.MaxTimeout = math.MaxInt64 + } for _, opt := range []ssh.Option{ // custom PublicKeyAuth handler @@ -157,6 +169,6 @@ func server(c *configServe) (err error) { } } - log.Printf("info: SSH Server accepting connections on %s", c.bindAddr) + log.Printf("info: SSH Server accepting connections on %s, idle-timout=%v", c.bindAddr, c.idleTimeout) return srv.Serve(ln) } diff --git a/pkg/bastionsession/bastionsession.go b/pkg/bastionsession/bastionsession.go index 76465a0..b37c0fc 100644 --- a/pkg/bastionsession/bastionsession.go +++ b/pkg/bastionsession/bastionsession.go @@ -5,14 +5,24 @@ import ( "io" "log" "os" + "path/filepath" + "strconv" "strings" "time" "github.com/arkan/bastion/pkg/logchannel" "github.com/gliderlabs/ssh" + "github.com/moul/sshportal/pkg/logtunnel" gossh "golang.org/x/crypto/ssh" ) +type ForwardData struct { + DestinationHost string + DestinationPort uint32 + SourceHost string + SourcePort uint32 +} + type Config struct { Addr string Logs string @@ -20,76 +30,141 @@ type Config struct { } func MultiChannelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context, configs []Config) error { - if newChan.ChannelType() != "session" { - newChan.Reject(gossh.UnknownChannelType, "unsupported channel type") - return nil - } - lch, lreqs, err := newChan.Accept() - // TODO: defer clean closer - if err != nil { - // TODO: trigger event callback - return nil - } - var lastClient *gossh.Client - - // go through all the hops - for _, config := range configs { - var client *gossh.Client - if lastClient == nil { - client, err = gossh.Dial("tcp", config.Addr, config.ClientConfig) - } else { - rconn, err := lastClient.Dial("tcp", config.Addr) - if err != nil { - return err - } - ncc, chans, reqs, err := gossh.NewClientConn(rconn, config.Addr, config.ClientConfig) - if err != nil { - return err - } - client = gossh.NewClient(ncc, chans, reqs) + switch newChan.ChannelType() { + case "session": + lch, lreqs, err := newChan.Accept() + // TODO: defer clean closer + if err != nil { + // TODO: trigger event callback + return nil } + + // go through all the hops + for _, config := range configs { + var client *gossh.Client + if lastClient == nil { + client, err = gossh.Dial("tcp", config.Addr, config.ClientConfig) + } else { + rconn, err := lastClient.Dial("tcp", config.Addr) + if err != nil { + return err + } + ncc, chans, reqs, err := gossh.NewClientConn(rconn, config.Addr, config.ClientConfig) + if err != nil { + return err + } + client = gossh.NewClient(ncc, chans, reqs) + } + if err != nil { + return err + } + defer func() { _ = client.Close() }() + lastClient = client + } + + rch, rreqs, err := lastClient.OpenChannel("session", []byte{}) if err != nil { return err } - defer func() { _ = client.Close() }() - lastClient = client + user := conn.User() + // pipe everything + return pipe(lreqs, rreqs, lch, rch, configs[len(configs)-1].Logs, user, newChan) + case "direct-tcpip": + lch, lreqs, err := newChan.Accept() + // TODO: defer clean closer + if err != nil { + // TODO: trigger event callback + return nil + } + + // go through all the hops + for _, config := range configs { + var client *gossh.Client + if lastClient == nil { + client, err = gossh.Dial("tcp", config.Addr, config.ClientConfig) + } else { + rconn, err := lastClient.Dial("tcp", config.Addr) + if err != nil { + return err + } + ncc, chans, reqs, err := gossh.NewClientConn(rconn, config.Addr, config.ClientConfig) + if err != nil { + return err + } + client = gossh.NewClient(ncc, chans, reqs) + } + if err != nil { + return err + } + defer func() { _ = client.Close() }() + lastClient = client + } + + d := logtunnel.ForwardData{} + if err := gossh.Unmarshal(newChan.ExtraData(), &d); err != nil { + return err + } + rch, rreqs, err := lastClient.OpenChannel("direct-tcpip", newChan.ExtraData()) + if err != nil { + return err + } + user := conn.User() + // pipe everything + return pipe(lreqs, rreqs, lch, rch, configs[len(configs)-1].Logs, user, newChan) + default: + newChan.Reject(gossh.UnknownChannelType, "unsupported channel type") + return nil } - rch, rreqs, err := lastClient.OpenChannel("session", []byte{}) - if err != nil { - return err - } - user := conn.User() - // pipe everything - return pipe(lreqs, rreqs, lch, rch, configs[len(configs)-1].Logs, user) } -func pipe(lreqs, rreqs <-chan *gossh.Request, lch, rch gossh.Channel, logsLocation string, user string) error { +func pipe(lreqs, rreqs <-chan *gossh.Request, lch, rch gossh.Channel, logsLocation string, user string, newChan gossh.NewChannel) error { defer func() { _ = lch.Close() _ = rch.Close() }() errch := make(chan error, 1) - file_name := strings.Join([]string{logsLocation, "/", user, "-", time.Now().Format(time.RFC3339)}, "") // get user + channeltype := newChan.ChannelType() + fileNameUnix := strings.Join([]string{logsLocation, "/", user, "-", channeltype, "-", strconv.FormatInt(time.Now().UnixNano(), 10)}, "") // get user + file_name := filepath.FromSlash(fileNameUnix) f, err := os.OpenFile(file_name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0640) + defer f.Close() + if err != nil { log.Fatalf("error: %v", err) } - log.Printf("Session is recorded in %v", file_name) - wrappedlch := logchannel.New(lch, f) - go func() { - _, _ = io.Copy(wrappedlch, rch) - errch <- errors.New("lch closed the connection") - }() + log.Printf("Session %v is recorded in %v", channeltype, file_name) + if channeltype == "session" { + wrappedlch := logchannel.New(lch, f) + go func() { + _, _ = io.Copy(wrappedlch, rch) + errch <- errors.New("lch closed the connection") + }() - defer f.Close() + go func() { + _, _ = io.Copy(rch, lch) + errch <- errors.New("rch closed the connection") + }() + } + if channeltype == "direct-tcpip" { + d := logtunnel.ForwardData{} + if err := gossh.Unmarshal(newChan.ExtraData(), &d); err != nil { + return err + } + wrappedlch := logtunnel.New(lch, f, d.SourceHost) + wrappedrch := logtunnel.New(rch, f, d.DestinationHost) + go func() { + _, _ = io.Copy(wrappedlch, rch) + errch <- errors.New("lch closed the connection") + }() - go func() { - _, _ = io.Copy(rch, lch) - errch <- errors.New("rch closed the connection") - }() + go func() { + _, _ = io.Copy(wrappedrch, lch) + errch <- errors.New("rch closed the connection") + }() + } for { select { @@ -98,6 +173,12 @@ func pipe(lreqs, rreqs <-chan *gossh.Request, lch, rch gossh.Channel, logsLocati return nil } b, err := rch.SendRequest(req.Type, req.WantReply, req.Payload) + if req.Type == "exec" { + wrappedlch := logchannel.New(lch, f) + command := append(req.Payload, []byte("\n")...) + wrappedlch.LogWrite(command) + } + if err != nil { return err } diff --git a/pkg/logtunnel/logtunnel.go b/pkg/logtunnel/logtunnel.go new file mode 100644 index 0000000..4c6f5c7 --- /dev/null +++ b/pkg/logtunnel/logtunnel.go @@ -0,0 +1,59 @@ +package logtunnel + +import ( + "encoding/binary" + "io" + "syscall" + "time" + + "golang.org/x/crypto/ssh" +) + +type logTunnel struct { + host string + channel ssh.Channel + writer io.WriteCloser +} + +type ForwardData struct { + DestinationHost string + DestinationPort uint32 + SourceHost string + SourcePort uint32 +} + +func writeHeader(fd io.Writer, length int) { + t := time.Now() + + tv := syscall.NsecToTimeval(t.UnixNano()) + + binary.Write(fd, binary.LittleEndian, int32(tv.Sec)) + binary.Write(fd, binary.LittleEndian, int32(tv.Usec)) + binary.Write(fd, binary.LittleEndian, int32(length)) +} + +func New(channel ssh.Channel, writer io.WriteCloser, host string) *logTunnel { + return &logTunnel{ + host: host, + channel: channel, + writer: writer, + } +} + +func (l *logTunnel) Read(data []byte) (int, error) { + return l.Read(data) +} + +func (l *logTunnel) Write(data []byte) (int, error) { + writeHeader(l.writer, len(data) + len(l.host + ": ")) + l.writer.Write([]byte(l.host + ": ")) + l.writer.Write(data) + + return l.channel.Write(data) +} + +func (l *logTunnel) Close() error { + l.writer.Close() + + return l.channel.Close() +} diff --git a/shell.go b/shell.go index 7b59b26..d82e1a3 100644 --- a/shell.go +++ b/shell.go @@ -32,6 +32,10 @@ var banner = ` ` var startTime = time.Now() +const ( + naMessage = "n/a" +) + func shell(s ssh.Session) error { var ( sshCommand = s.Command() @@ -1091,6 +1095,47 @@ GLOBAL OPTIONS: return HostGroupsByIdentifiers(db, c.Args()).Delete(&HostGroup{}).Error }, + }, { + Name: "update", + Usage: "Updates a host group", + ArgsUsage: "HOSTGROUP...", + Flags: []cli.Flag{ + cli.StringFlag{Name: "name", Usage: "Assigns a new name to the host group"}, + cli.StringFlag{Name: "comment", Usage: "Adds a comment"}, + }, + Action: func(c *cli.Context) error { + if c.NArg() < 1 { + return cli.ShowSubcommandHelp(c) + } + + if err := myself.CheckRoles([]string{"admin"}); err != nil { + return err + } + + var hostgroups []HostGroup + if err := HostGroupsByIdentifiers(db, c.Args()).Find(&hostgroups).Error; err != nil { + return err + } + + if len(hostgroups) > 1 && c.String("name") != "" { + return fmt.Errorf("cannot set --name when editing multiple hostgroups at once") + } + + tx := db.Begin() + for _, hostgroup := range hostgroups { + model := tx.Model(&hostgroup) + // simple fields + for _, fieldname := range []string{"name", "comment"} { + if c.String(fieldname) != "" { + if err := model.Update(fieldname, c.String(fieldname)).Error; err != nil { + tx.Rollback() + return err + } + } + } + } + return tx.Commit().Error + }, }, }, }, { @@ -1178,6 +1223,60 @@ GLOBAL OPTIONS: return nil }, }, { + Name: "import", + Usage: "Imports an existing private key", + Description: "$> key import\n $> key import --name=mykey", + Flags: []cli.Flag{ + cli.StringFlag{Name: "name", Usage: "Assigns a name to the key"}, + cli.StringFlag{Name: "comment", Usage: "Adds a comment"}, + }, + Action: func(c *cli.Context) error { + if err := myself.CheckRoles([]string{"admin"}); err != nil { + return err + } + + var name string + if c.String("name") != "" { + name = c.String("name") + } else { + name = namesgenerator.GetRandomName(0) + } + + var value string + term := terminal.NewTerminal(s, "Paste your key and end with a blank line> ") + for { + line, err := term.ReadLine() + if err != nil { + return err + } + if line != "" { + value += line + "\n" + } else { + break + } + } + key, err := ImportSSHKey(value) + if err != nil { + return err + } + + key.Name = name + key.Comment = c.String("comment") + + if _, err := govalidator.ValidateStruct(key); err != nil { + return err + } + // FIXME: check if name already exists + + // save the key in database + if err := db.Create(&key).Error; err != nil { + return err + } + fmt.Fprintf(s, "%d\n", key.ID) + + return nil + }, + }, { Name: "inspect", Usage: "Shows detailed information on one or more keys", ArgsUsage: "KEY...", @@ -1746,6 +1845,47 @@ GLOBAL OPTIONS: return UserGroupsByIdentifiers(db, c.Args()).Delete(&UserGroup{}).Error }, + }, { + Name: "update", + Usage: "Updates a user group", + ArgsUsage: "USERGROUP...", + Flags: []cli.Flag{ + cli.StringFlag{Name: "name", Usage: "Assigns a new name to the user group"}, + cli.StringFlag{Name: "comment", Usage: "Adds a comment"}, + }, + Action: func(c *cli.Context) error { + if c.NArg() < 1 { + return cli.ShowSubcommandHelp(c) + } + + if err := myself.CheckRoles([]string{"admin"}); err != nil { + return err + } + + var usergroups []UserGroup + if err := UserGroupsByIdentifiers(db, c.Args()).Find(&usergroups).Error; err != nil { + return err + } + + if len(usergroups) > 1 && c.String("name") != "" { + return fmt.Errorf("cannot set --name when editing multiple usergroups at once") + } + + tx := db.Begin() + for _, usergroup := range usergroups { + model := tx.Model(&usergroup) + // simple fields + for _, fieldname := range []string{"name", "comment"} { + if c.String(fieldname) != "" { + if err := model.Update(fieldname, c.String(fieldname)).Error; err != nil { + tx.Rollback() + return err + } + } + } + } + return tx.Commit().Error + }, }, }, }, { @@ -1862,9 +2002,13 @@ GLOBAL OPTIONS: table.SetBorder(false) table.SetCaption(true, fmt.Sprintf("Total: %d userkeys.", len(userKeys))) for _, userkey := range userKeys { + email := naMessage + if userkey.User != nil { + email = userkey.User.Email + } table.Append([]string{ fmt.Sprintf("%d", userkey.ID), - userkey.User.Email, + email, // FIXME: add fingerprint humanize.Time(userkey.UpdatedAt), humanize.Time(userkey.CreatedAt), @@ -1961,10 +2105,18 @@ GLOBAL OPTIONS: duration = humanize.RelTime(session.CreatedAt, *session.StoppedAt, "", "") } duration = strings.Replace(duration, "now", "1 second", 1) + hostname := naMessage + if session.Host != nil { + hostname = session.Host.Name + } + username := naMessage + if session.User != nil { + username = session.User.Name + } table.Append([]string{ fmt.Sprintf("%d", session.ID), - session.User.Name, - session.Host.Name, + username, + hostname, session.Status, humanize.Time(session.CreatedAt), duration, diff --git a/ssh.go b/ssh.go index 9e632dc..d68e915 100644 --- a/ssh.go +++ b/ssh.go @@ -86,6 +86,7 @@ func dynamicHostKey(db *gorm.DB, host *Host) gossh.HostKeyCallback { func channelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context) { switch newChan.ChannelType() { case "session": + case "direct-tcpip": default: // TODO: handle direct-tcp (only for ssh scheme) if err := newChan.Reject(gossh.UnknownChannelType, "unsupported channel type"); err != nil { @@ -157,7 +158,12 @@ func channelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewCh return } - err = bastionsession.MultiChannelHandler(srv, conn, newChan, ctx, sessionConfigs) + go func() { + err = bastionsession.MultiChannelHandler(srv, conn, newChan, ctx, sessionConfigs) + if err != nil { + log.Printf("Error: %v", err) + } + }() now := time.Now() sessUpdate := Session{ diff --git a/vendor/github.com/arkan/bastion/pkg/logchannel/logchannel.go b/vendor/github.com/arkan/bastion/pkg/logchannel/logchannel.go index e62355b..27ee28f 100644 --- a/vendor/github.com/arkan/bastion/pkg/logchannel/logchannel.go +++ b/vendor/github.com/arkan/bastion/pkg/logchannel/logchannel.go @@ -42,6 +42,11 @@ func (l *logChannel) Write(data []byte) (int, error) { return l.channel.Write(data) } +func (l *logChannel) LogWrite(data []byte) (int, error) { + writeTTYRecHeader(l.writer, len(data)) + return l.writer.Write(data) +} + func (l *logChannel) Close() error { l.writer.Close()