session.go 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. package sshd
  2. import (
  3. "errors"
  4. "log/slog"
  5. "strings"
  6. "github.com/slackhq/nebula/commands"
  7. "golang.org/x/crypto/ssh"
  8. "golang.org/x/term"
  9. )
  10. type session struct {
  11. l *slog.Logger
  12. c *ssh.ServerConn
  13. term *term.Terminal
  14. commands *commands.Registry
  15. cancel func()
  16. }
  17. func NewSession(reg *commands.Registry, conn *ssh.ServerConn, chans <-chan ssh.NewChannel, cancel func(), l *slog.Logger) *session {
  18. s := &session{
  19. commands: reg.Clone(),
  20. l: l,
  21. c: conn,
  22. cancel: cancel,
  23. }
  24. s.commands.Register(LogoutCommand(s.Close))
  25. go s.handleChannels(chans)
  26. return s
  27. }
  28. // LogoutCommand is the logout command a session registers on its clone of the registry,
  29. // with end as what it does. Exported so a test can serve what a session serves.
  30. func LogoutCommand(end func()) *commands.Command {
  31. return &commands.Command{
  32. Name: commands.Logout,
  33. ShortDescription: "Ends the current session",
  34. Callback: func(fs any, a []string, w commands.StringWriter) error {
  35. end()
  36. return nil
  37. },
  38. }
  39. }
  40. func (s *session) handleChannels(chans <-chan ssh.NewChannel) {
  41. defer s.Close()
  42. for newChannel := range chans {
  43. if newChannel.ChannelType() != "session" {
  44. s.l.Error("unknown channel type", "sshChannelType", newChannel.ChannelType())
  45. newChannel.Reject(ssh.UnknownChannelType, "unknown channel type")
  46. continue
  47. }
  48. channel, requests, err := newChannel.Accept()
  49. if err != nil {
  50. s.l.Warn("could not accept channel", "error", err)
  51. continue
  52. }
  53. go s.handleRequests(requests, channel)
  54. }
  55. }
  56. func (s *session) handleRequests(in <-chan *ssh.Request, channel ssh.Channel) {
  57. for req := range in {
  58. var err error
  59. switch req.Type {
  60. case "shell":
  61. if s.term == nil {
  62. s.term = s.createTerm(channel)
  63. err = req.Reply(true, nil)
  64. } else {
  65. err = req.Reply(false, nil)
  66. }
  67. case "pty-req":
  68. err = req.Reply(true, nil)
  69. case "window-change":
  70. err = req.Reply(true, nil)
  71. case "exec":
  72. var payload = struct{ Value string }{}
  73. cErr := ssh.Unmarshal(req.Payload, &payload)
  74. if cErr != nil {
  75. req.Reply(false, nil)
  76. return
  77. }
  78. req.Reply(true, nil)
  79. err := s.dispatchCommand(payload.Value, commands.NewStringWriter(channel))
  80. // A command that failed exits 1, so `ssh host cmd` callers can tell.
  81. status := struct{ Status uint32 }{uint32(0)}
  82. if err != nil {
  83. status.Status = 1
  84. }
  85. channel.SendRequest("exit-status", false, ssh.Marshal(status))
  86. channel.Close()
  87. return
  88. default:
  89. s.l.Debug("Rejected unknown request", "sshRequest", req.Type)
  90. err = req.Reply(false, nil)
  91. }
  92. if err != nil {
  93. s.l.Info("Error handling ssh session requests", "error", err)
  94. return
  95. }
  96. }
  97. }
  98. func (s *session) createTerm(channel ssh.Channel) *term.Terminal {
  99. term := term.NewTerminal(channel, s.c.User()+"@nebula > ")
  100. term.AutoCompleteCallback = func(line string, pos int, key rune) (newLine string, newPos int, ok bool) {
  101. // key 9 is tab
  102. if key == 9 {
  103. cmds := s.commands.Match(line)
  104. if len(cmds) == 1 {
  105. return cmds[0] + " ", len(cmds[0]) + 1, true
  106. }
  107. term.Write([]byte(strings.Join(cmds, "\n") + "\n\n"))
  108. }
  109. return "", 0, false
  110. }
  111. go s.handleInput()
  112. return term
  113. }
  114. func (s *session) handleInput() {
  115. w := commands.NewStringWriter(s.term)
  116. for {
  117. line, err := s.term.ReadLine()
  118. if err != nil {
  119. break
  120. }
  121. s.dispatchCommand(line, w)
  122. }
  123. }
  124. // dispatchCommand runs line and returns the command's error, which it has logged unless
  125. // the command already told the user everything: the log line is for the operator reading
  126. // the daemon's log, who learns nothing from a failure the user caused.
  127. func (s *session) dispatchCommand(line string, w commands.StringWriter) error {
  128. err := s.commands.Dispatch(line, w)
  129. if err == nil {
  130. return nil
  131. }
  132. var f *commands.Failure
  133. if !errors.As(err, &f) {
  134. s.l.Debug("Command returned an error", "line", line, "error", err)
  135. }
  136. return err
  137. }
  138. func (s *session) Close() {
  139. s.c.Close()
  140. s.cancel()
  141. }