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