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