1package ssh
 2
 3import (
 4	"strings"
 5
 6	tea "github.com/charmbracelet/bubbletea"
 7	"github.com/charmbracelet/log"
 8	. "github.com/charmbracelet/soft-serve/internal/log"
 9	"github.com/charmbracelet/soft-serve/server/backend"
10	"github.com/charmbracelet/soft-serve/server/config"
11	"github.com/charmbracelet/soft-serve/server/errors"
12	"github.com/charmbracelet/soft-serve/server/ui"
13	"github.com/charmbracelet/soft-serve/server/ui/common"
14	"github.com/charmbracelet/ssh"
15	"github.com/charmbracelet/wish"
16	bm "github.com/charmbracelet/wish/bubbletea"
17	"github.com/muesli/termenv"
18	"github.com/prometheus/client_golang/prometheus"
19	"github.com/prometheus/client_golang/prometheus/promauto"
20)
21
22var (
23	tuiSessionCounter = promauto.NewCounterVec(prometheus.CounterOpts{
24		Namespace: "soft_serve",
25		Subsystem: "ssh",
26		Name:      "tui_session_total",
27		Help:      "The total number of TUI sessions",
28	}, []string{"key", "user", "repo", "term"})
29)
30
31// SessionHandler is the soft-serve bubbletea ssh session handler.
32func SessionHandler(cfg *config.Config) bm.ProgramHandler {
33	return func(s ssh.Session) *tea.Program {
34		ak := backend.MarshalAuthorizedKey(s.PublicKey())
35		pty, _, active := s.Pty()
36		if !active {
37			return nil
38		}
39
40		cmd := s.Command()
41		initialRepo := ""
42		if len(cmd) == 1 {
43			initialRepo = cmd[0]
44			auth := cfg.Backend.AccessLevelByPublicKey(initialRepo, s.PublicKey())
45			if auth < backend.ReadOnlyAccess {
46				wish.Fatalln(s, errors.ErrUnauthorized)
47				return nil
48			}
49		}
50
51		envs := &sessionEnv{s}
52		output := termenv.NewOutput(s, termenv.WithColorCache(true), termenv.WithEnvironment(envs))
53		logger := NewDefaultLogger()
54		ctx := log.WithContext(s.Context(), logger)
55		c := common.NewCommon(ctx, output, pty.Window.Width, pty.Window.Height)
56		c.SetValue(common.ConfigKey, cfg)
57		m := ui.New(c, initialRepo)
58		p := tea.NewProgram(m,
59			tea.WithInput(s),
60			tea.WithOutput(s),
61			tea.WithAltScreen(),
62			tea.WithoutCatchPanics(),
63			tea.WithMouseCellMotion(),
64		)
65
66		tuiSessionCounter.WithLabelValues(ak, s.User(), initialRepo, pty.Term).Inc()
67
68		return p
69	}
70}
71
72var _ termenv.Environ = &sessionEnv{}
73
74type sessionEnv struct {
75	ssh.Session
76}
77
78func (s *sessionEnv) Environ() []string {
79	pty, _, _ := s.Pty()
80	return append(s.Session.Environ(), "TERM="+pty.Term)
81}
82
83func (s *sessionEnv) Getenv(key string) string {
84	for _, env := range s.Environ() {
85		if strings.HasPrefix(env, key+"=") {
86			return strings.TrimPrefix(env, key+"=")
87		}
88	}
89	return ""
90}