middleware.go

  1package ssh
  2
  3import (
  4	"github.com/charmbracelet/log"
  5	"github.com/charmbracelet/soft-serve/server/backend"
  6	"github.com/charmbracelet/soft-serve/server/config"
  7	"github.com/charmbracelet/soft-serve/server/db"
  8	"github.com/charmbracelet/soft-serve/server/proto"
  9	"github.com/charmbracelet/soft-serve/server/ssh/cmd"
 10	"github.com/charmbracelet/soft-serve/server/sshutils"
 11	"github.com/charmbracelet/soft-serve/server/store"
 12	"github.com/charmbracelet/ssh"
 13	"github.com/prometheus/client_golang/prometheus"
 14	"github.com/prometheus/client_golang/prometheus/promauto"
 15	"github.com/spf13/cobra"
 16)
 17
 18// ContextMiddleware adds the config, backend, and logger to the session context.
 19func ContextMiddleware(cfg *config.Config, dbx *db.DB, datastore store.Store, be *backend.Backend, logger *log.Logger) func(ssh.Handler) ssh.Handler {
 20	return func(sh ssh.Handler) ssh.Handler {
 21		return func(s ssh.Session) {
 22			s.Context().SetValue(sshutils.ContextKeySession, s)
 23			s.Context().SetValue(config.ContextKey, cfg)
 24			s.Context().SetValue(db.ContextKey, dbx)
 25			s.Context().SetValue(store.ContextKey, datastore)
 26			s.Context().SetValue(backend.ContextKey, be)
 27			s.Context().SetValue(log.ContextKey, logger.WithPrefix("ssh"))
 28			sh(s)
 29		}
 30	}
 31}
 32
 33var cliCommandCounter = promauto.NewCounterVec(prometheus.CounterOpts{
 34	Namespace: "soft_serve",
 35	Subsystem: "cli",
 36	Name:      "commands_total",
 37	Help:      "Total times each command was called",
 38}, []string{"command"})
 39
 40// CommandMiddleware handles git commands and CLI commands.
 41// This middleware must be run after the ContextMiddleware.
 42func CommandMiddleware(sh ssh.Handler) ssh.Handler {
 43	return func(s ssh.Session) {
 44		func() {
 45			_, _, ptyReq := s.Pty()
 46			if ptyReq {
 47				return
 48			}
 49
 50			ctx := s.Context()
 51			cfg := config.FromContext(ctx)
 52			logger := log.FromContext(ctx)
 53
 54			args := s.Command()
 55			cliCommandCounter.WithLabelValues(cmd.CommandName(args)).Inc()
 56			rootCmd := &cobra.Command{
 57				Short:        "Soft Serve is a self-hostable Git server for the command line.",
 58				SilenceUsage: true,
 59			}
 60			rootCmd.CompletionOptions.DisableDefaultCmd = true
 61
 62			rootCmd.SetUsageTemplate(cmd.UsageTemplate)
 63			rootCmd.SetUsageFunc(cmd.UsageFunc)
 64			rootCmd.AddCommand(
 65				cmd.GitUploadPackCommand(),
 66				cmd.GitUploadArchiveCommand(),
 67				cmd.GitReceivePackCommand(),
 68				cmd.RepoCommand(),
 69			)
 70
 71			if cfg.LFS.Enabled {
 72				rootCmd.AddCommand(
 73					cmd.GitLFSAuthenticateCommand(),
 74				)
 75
 76				if cfg.LFS.SSHEnabled {
 77					rootCmd.AddCommand(
 78						cmd.GitLFSTransfer(),
 79					)
 80				}
 81			}
 82
 83			rootCmd.SetArgs(args)
 84			if len(args) == 0 {
 85				// otherwise it'll default to os.Args, which is not what we want.
 86				rootCmd.SetArgs([]string{"--help"})
 87			}
 88			rootCmd.SetIn(s)
 89			rootCmd.SetOut(s)
 90			rootCmd.SetErr(s.Stderr())
 91			rootCmd.SetContext(ctx)
 92
 93			user := proto.UserFromContext(ctx)
 94			isAdmin := cmd.IsPublicKeyAdmin(cfg, s.PublicKey()) || (user != nil && user.IsAdmin())
 95			if user != nil || isAdmin {
 96				if isAdmin {
 97					rootCmd.AddCommand(
 98						cmd.SettingsCommand(),
 99						cmd.UserCommand(),
100					)
101				}
102
103				rootCmd.AddCommand(
104					cmd.InfoCommand(),
105					cmd.PubkeyCommand(),
106					cmd.SetUsernameCommand(),
107					cmd.JWTCommand(),
108					cmd.TokenCommand(),
109				)
110			}
111
112			if err := rootCmd.ExecuteContext(ctx); err != nil {
113				logger.Error("error executing command", "err", err)
114				s.Exit(1) // nolint: errcheck
115				return
116			}
117		}()
118		sh(s)
119	}
120}