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