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}