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}