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}