| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168 |
- package sshd
- import (
- "errors"
- "log/slog"
- "strings"
- "github.com/slackhq/nebula/commands"
- "golang.org/x/crypto/ssh"
- "golang.org/x/term"
- )
- type session struct {
- l *slog.Logger
- c *ssh.ServerConn
- term *term.Terminal
- commands *commands.Registry
- cancel func()
- }
- func NewSession(reg *commands.Registry, conn *ssh.ServerConn, chans <-chan ssh.NewChannel, cancel func(), l *slog.Logger) *session {
- s := &session{
- commands: reg.Clone(),
- l: l,
- c: conn,
- cancel: cancel,
- }
- s.commands.Register(LogoutCommand(s.Close))
- go s.handleChannels(chans)
- return s
- }
- // LogoutCommand is the logout command a session registers on its clone of the registry,
- // with end as what it does. Exported so a test can serve what a session serves.
- func LogoutCommand(end func()) *commands.Command {
- return &commands.Command{
- Name: commands.Logout,
- ShortDescription: "Ends the current session",
- Callback: func(fs any, a []string, w commands.StringWriter) error {
- end()
- return nil
- },
- }
- }
- func (s *session) handleChannels(chans <-chan ssh.NewChannel) {
- defer s.Close()
- for newChannel := range chans {
- if newChannel.ChannelType() != "session" {
- s.l.Error("unknown channel type", "sshChannelType", newChannel.ChannelType())
- newChannel.Reject(ssh.UnknownChannelType, "unknown channel type")
- continue
- }
- channel, requests, err := newChannel.Accept()
- if err != nil {
- s.l.Warn("could not accept channel", "error", err)
- continue
- }
- go s.handleRequests(requests, channel)
- }
- }
- func (s *session) handleRequests(in <-chan *ssh.Request, channel ssh.Channel) {
- for req := range in {
- var err error
- switch req.Type {
- case "shell":
- if s.term == nil {
- s.term = s.createTerm(channel)
- err = req.Reply(true, nil)
- } else {
- err = req.Reply(false, nil)
- }
- case "pty-req":
- err = req.Reply(true, nil)
- case "window-change":
- err = req.Reply(true, nil)
- case "exec":
- var payload = struct{ Value string }{}
- cErr := ssh.Unmarshal(req.Payload, &payload)
- if cErr != nil {
- req.Reply(false, nil)
- return
- }
- req.Reply(true, nil)
- err := s.dispatchCommand(payload.Value, commands.NewStringWriter(channel))
- // A command that failed exits 1, so `ssh host cmd` callers can tell.
- status := struct{ Status uint32 }{uint32(0)}
- if err != nil {
- status.Status = 1
- }
- channel.SendRequest("exit-status", false, ssh.Marshal(status))
- channel.Close()
- return
- default:
- s.l.Debug("Rejected unknown request", "sshRequest", req.Type)
- err = req.Reply(false, nil)
- }
- if err != nil {
- s.l.Info("Error handling ssh session requests", "error", err)
- return
- }
- }
- }
- func (s *session) createTerm(channel ssh.Channel) *term.Terminal {
- term := term.NewTerminal(channel, s.c.User()+"@nebula > ")
- term.AutoCompleteCallback = func(line string, pos int, key rune) (newLine string, newPos int, ok bool) {
- // key 9 is tab
- if key == 9 {
- cmds := s.commands.Match(line)
- if len(cmds) == 1 {
- return cmds[0] + " ", len(cmds[0]) + 1, true
- }
- term.Write([]byte(strings.Join(cmds, "\n") + "\n\n"))
- }
- return "", 0, false
- }
- go s.handleInput()
- return term
- }
- func (s *session) handleInput() {
- w := commands.NewStringWriter(s.term)
- for {
- line, err := s.term.ReadLine()
- if err != nil {
- break
- }
- s.dispatchCommand(line, w)
- }
- }
- // dispatchCommand runs line and returns the command's error, which it has logged unless
- // the command already told the user everything: the log line is for the operator reading
- // the daemon's log, who learns nothing from a failure the user caused.
- func (s *session) dispatchCommand(line string, w commands.StringWriter) error {
- err := s.commands.Dispatch(line, w)
- if err == nil {
- return nil
- }
- var f *commands.Failure
- if !errors.As(err, &f) {
- s.l.Debug("Command returned an error", "line", line, "error", err)
- }
- return err
- }
- func (s *session) Close() {
- s.c.Close()
- s.cancel()
- }
|