session.go

 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}