1package ssh
  2
  3import (
  4	"strings"
  5	"time"
  6
  7	tea "github.com/charmbracelet/bubbletea"
  8	"github.com/charmbracelet/lipgloss"
  9	"github.com/charmbracelet/soft-serve/pkg/access"
 10	"github.com/charmbracelet/soft-serve/pkg/backend"
 11	"github.com/charmbracelet/soft-serve/pkg/config"
 12	"github.com/charmbracelet/soft-serve/pkg/proto"
 13	"github.com/charmbracelet/soft-serve/pkg/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 := lipgloss.NewRenderer(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 := NewUI(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}