b26060bae203a139fd62e60e4760d78e71ab389c
- Author
- Ayman Bagabas <ayman.bagabas@gmail.com>
- Committer
- Ayman Bagabas <ayman.bagabas@gmail.com>
- Date
Message
Diff
This diff is truncated to protect this page.
1diff --git a/server/ssh/cmd.go b/server/ssh/cmd.go
2deleted file mode 100644
3index f2563f84208aaed4932bc12b40ab89950537758b..0000000000000000000000000000000000000000
4--- a/server/ssh/cmd.go
5+++ /dev/null
6@@ -1,17 +0,0 @@
7-package ssh
8-
9-import (
10- "github.com/charmbracelet/log"
11- "github.com/charmbracelet/soft-serve/server/ssh/cmd"
12- "github.com/charmbracelet/ssh"
13-)
14-
15-func handleCli(s ssh.Session) {
16- ctx := s.Context()
17- logger := log.FromContext(ctx)
18- rootCmd := cmd.RootCommand(s)
19- if err := rootCmd.ExecuteContext(ctx); err != nil {
20- logger.Error("error executing command", "err", err)
21- _ = s.Exit(1)
22- }
23-}
24diff --git a/server/ssh/cmd/cmd.go b/server/ssh/cmd/cmd.go
25index 099102a650fa330d789896703ff81e1dc9d05d7b..08ac8f9b4312297fd704bc296117ba05c1e8091d 100644
26--- a/server/ssh/cmd/cmd.go
27+++ b/server/ssh/cmd/cmd.go
28@@ -14,18 +14,9 @@ import (
29 "github.com/charmbracelet/soft-serve/server/sshutils"
30 "github.com/charmbracelet/soft-serve/server/utils"
31 "github.com/charmbracelet/ssh"
32- "github.com/prometheus/client_golang/prometheus"
33- "github.com/prometheus/client_golang/prometheus/promauto"
34 "github.com/spf13/cobra"
35 )
36
37-var cliCommandCounter = promauto.NewCounterVec(prometheus.CounterOpts{
38- Namespace: "soft_serve",
39- Subsystem: "cli",
40- Name: "commands_total",
41- Help: "Total times each command was called",
42-}, []string{"command"})
43-
44 var templateFuncs = template.FuncMap{
45 "trim": strings.TrimSpace,
46 "trimRightSpace": trimRightSpace,
47@@ -36,7 +27,8 @@ var templateFuncs = template.FuncMap{
48 }
49
50 const (
51- usageTmpl = `Usage:{{if .Runnable}}
52+ // UsageTemplate is the template used for the help output.
53+ UsageTemplate = `Usage:{{if .Runnable}}
54 {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}
55 {{.SSHCommand}}{{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}}
56
57@@ -68,35 +60,11 @@ Use "{{.SSHCommand}}{{.CommandPath}} [command] --help" for more information abou
58 `
59 )
60
61-func trimRightSpace(s string) string {
62- return strings.TrimRightFunc(s, unicode.IsSpace)
63-}
64-
65-// rpad adds padding to the right of a string.
66-func rpad(s string, padding int) string {
67- template := fmt.Sprintf("%%-%ds", padding)
68- return fmt.Sprintf(template, s)
69-}
70-
71-func cmdName(args []string) string {
72- if len(args) == 0 {
73- return ""
74- }
75- return args[0]
76-}
77-
78-// RootCommand returns a new cli root command.
79-func RootCommand(s ssh.Session) *cobra.Command {
80- ctx := s.Context()
81+// UsageFunc is a function that can be used as a cobra.Command's
82+// UsageFunc to render the help output.
83+func UsageFunc(c *cobra.Command) error {
84+ ctx := c.Context()
85 cfg := config.FromContext(ctx)
86-
87- args := s.Command()
88- cliCommandCounter.WithLabelValues(cmdName(args)).Inc()
89- rootCmd := &cobra.Command{
90- Short: "Soft Serve is a self-hostable Git server for the command line.",
91- SilenceUsage: true,
92- }
93-
94 hostname := "localhost"
95 port := "23231"
96 url, err := url.Parse(cfg.SSH.PublicURL)
97@@ -111,54 +79,34 @@ func RootCommand(s ssh.Session) *cobra.Command {
98 }
99
100 sshCmd += " " + hostname
101- rootCmd.SetUsageTemplate(usageTmpl)
102- rootCmd.SetUsageFunc(func(c *cobra.Command) error {
103- t := template.New("usage")
104- t.Funcs(templateFuncs)
105- template.Must(t.Parse(c.UsageTemplate()))
106- return t.Execute(c.OutOrStderr(), struct {
107- *cobra.Command
108- SSHCommand string
109- }{
110- Command: c,
111- SSHCommand: sshCmd,
112- })
113+ t := template.New("usage")
114+ t.Funcs(templateFuncs)
115+ template.Must(t.Parse(c.UsageTemplate()))
116+ return t.Execute(c.OutOrStderr(), struct {
117+ *cobra.Command
118+ SSHCommand string
119+ }{
120+ Command: c,
121+ SSHCommand: sshCmd,
122 })
123- rootCmd.CompletionOptions.DisableDefaultCmd = true
124- rootCmd.AddCommand(
125- repoCommand(),
126- )
127+}
128diff --git a/server/ssh/cmd/git.go b/server/ssh/cmd/git.go
129new file mode 100644
130index 0000000000000000000000000000000000000000..d2886514a9b4b7a8d7c9b0fbf752804353dbc5d8
131--- /dev/null
132+++ b/server/ssh/cmd/git.go
133@@ -0,0 +1,333 @@
134+package cmd
135+
136+import (
137+ "errors"
138+ "path/filepath"
139+ "time"
140+
141+ "github.com/charmbracelet/log"
142+ "github.com/charmbracelet/soft-serve/server/access"
143+ "github.com/charmbracelet/soft-serve/server/backend"
144+ "github.com/charmbracelet/soft-serve/server/config"
145+ "github.com/charmbracelet/soft-serve/server/git"
146+ "github.com/charmbracelet/soft-serve/server/lfs"
147+ "github.com/charmbracelet/soft-serve/server/proto"
148+ "github.com/charmbracelet/soft-serve/server/sshutils"
149+ "github.com/charmbracelet/soft-serve/server/utils"
150+ "github.com/prometheus/client_golang/prometheus"
151+ "github.com/prometheus/client_golang/prometheus/promauto"
152+ "github.com/spf13/cobra"
153+)
154+
155+var (
156+ uploadPackCounter = promauto.NewCounterVec(prometheus.CounterOpts{
157+ Namespace: "soft_serve",
158+ Subsystem: "git",
159+ Name: "upload_pack_total",
160+ Help: "The total number of git-upload-pack requests",
161+ }, []string{"repo"})
162+
163+ receivePackCounter = promauto.NewCounterVec(prometheus.CounterOpts{
164+ Namespace: "soft_serve",
165+ Subsystem: "git",
166+ Name: "receive_pack_total",
167+ Help: "The total number of git-receive-pack requests",
168+ }, []string{"repo"})
169+
170+ uploadArchiveCounter = promauto.NewCounterVec(prometheus.CounterOpts{
171+ Namespace: "soft_serve",
172+ Subsystem: "git",
173+ Name: "upload_archive_total",
174+ Help: "The total number of git-upload-archive requests",
175+ }, []string{"repo"})
176+
177+ lfsAuthenticateCounter = promauto.NewCounterVec(prometheus.CounterOpts{
178+ Namespace: "soft_serve",
179+ Subsystem: "git",
180+ Name: "lfs_authenticate_total",
181+ Help: "The total number of git-lfs-authenticate requests",
182+ }, []string{"repo", "operation"})
183+
184+ lfsTransferCounter = promauto.NewCounterVec(prometheus.CounterOpts{
185+ Namespace: "soft_serve",
186+ Subsystem: "git",
187+ Name: "lfs_transfer_total",
188+ Help: "The total number of git-lfs-transfer requests",
189+ }, []string{"repo", "operation"})
190+
191+ uploadPackSeconds = promauto.NewCounterVec(prometheus.CounterOpts{
192+ Namespace: "soft_serve",
193+ Subsystem: "git",
194+ Name: "upload_pack_seconds_total",
195+ Help: "The total time spent on git-upload-pack requests",
196+ }, []string{"repo"})
197+
198+ receivePackSeconds = promauto.NewCounterVec(prometheus.CounterOpts{
199+ Namespace: "soft_serve",
200+ Subsystem: "git",
201+ Name: "receive_pack_seconds_total",
202+ Help: "The total time spent on git-receive-pack requests",
203+ }, []string{"repo"})
204+
205+ uploadArchiveSeconds = promauto.NewCounterVec(prometheus.CounterOpts{
206+ Namespace: "soft_serve",
207+ Subsystem: "git",
208+ Name: "upload_archive_seconds_total",
209+ Help: "The total time spent on git-upload-archive requests",
210+ }, []string{"repo", "operation"})
211+
212+ lfsAuthenticateSeconds = promauto.NewCounterVec(prometheus.CounterOpts{
213+ Namespace: "soft_serve",
214+ Subsystem: "git",
215+ Name: "lfs_authenticate_seconds_total",
216+ Help: "The total time spent on git-lfs-authenticate requests",
217+ }, []string{"repo", "operation"})
218+
219+ lfsTransferSeconds = promauto.NewCounterVec(prometheus.CounterOpts{
220+ Namespace: "soft_serve",
221+ Subsystem: "git",
222+ Name: "lfs_transfer_seconds_total",
223+ Help: "The total time spent on git-lfs-transfer requests",
224+ }, []string{"repo"})
225+
226+ createRepoCounter = promauto.NewCounterVec(prometheus.CounterOpts{
227+ Namespace: "soft_serve",
228+ Subsystem: "ssh",
229+ Name: "create_repo_total",
230+ Help: "The total number of create repo requests",
231+ }, []string{"repo"})
232+)
233diff --git a/server/ssh/cmd/info.go b/server/ssh/cmd/info.go
234index 7b2ac0aeff29402323345fdfd284cea919c03cd0..43f0c15ec91eaa29322381fd2676fa6b35c55ddb 100644
235--- a/server/ssh/cmd/info.go
236+++ b/server/ssh/cmd/info.go
237@@ -6,12 +6,13 @@ import (
238 "github.com/spf13/cobra"
239 )
240
241-func infoCommand() *cobra.Command {
242+// InfoCommand returns a command that shows the user's info
243+func InfoCommand() *cobra.Command {
244 cmd := &cobra.Command{
245 Use: "info",
246 Short: "Show your info",
247 Args: cobra.NoArgs,
248- RunE: func(cmd *cobra.Command, args []string) error {
249+ RunE: func(cmd *cobra.Command, _ []string) error {
250 ctx := cmd.Context()
251 be := backend.FromContext(ctx)
252 pk := sshutils.PublicKeyFromContext(ctx)
253diff --git a/server/ssh/cmd/jwt.go b/server/ssh/cmd/jwt.go
254index b574889f5a56f9f3e56ae7c1daf76828bc4957ab..f5dec4cbf175fd2f7bfee87d410959236c723bd2 100644
255--- a/server/ssh/cmd/jwt.go
256+++ b/server/ssh/cmd/jwt.go
257@@ -11,7 +11,8 @@ import (
258 "github.com/spf13/cobra"
259 )
260
261-func jwtCommand() *cobra.Command {
262+// JWTCommand returns a command that generates a JSON Web Token.
263+func JWTCommand() *cobra.Command {
264 cmd := &cobra.Command{
265 Use: "jwt [repository1 repository2...]",
266 Short: "Generate a JSON Web Token",
267diff --git a/server/ssh/cmd/list.go b/server/ssh/cmd/list.go
268index 62334c871046f732d741bdb632adc07f0517ed06..539a4dfbcb4d98d297808b12ce8305d83532edbe 100644
269--- a/server/ssh/cmd/list.go
270+++ b/server/ssh/cmd/list.go
271@@ -16,7 +16,7 @@ func listCommand() *cobra.Command {
272 Aliases: []string{"ls"},
273 Short: "List repositories",
274 Args: cobra.NoArgs,
275- RunE: func(cmd *cobra.Command, args []string) error {
276+ RunE: func(cmd *cobra.Command, _ []string) error {
277 ctx := cmd.Context()
278 be := backend.FromContext(ctx)
279 pk := sshutils.PublicKeyFromContext(ctx)
280diff --git a/server/ssh/cmd/pubkey.go b/server/ssh/cmd/pubkey.go
281index e2200b1dc7d05dd878687c339654b5bfb9b4d2f7..11edd386d09980a2b2d6b33302839f8aa85932ff 100644
282--- a/server/ssh/cmd/pubkey.go
283+++ b/server/ssh/cmd/pubkey.go
284@@ -8,7 +8,8 @@ import (
285 "github.com/spf13/cobra"
286 )
287
288-func pubkeyCommand() *cobra.Command {
289+// PubkeyCommand returns a command that manages user public keys.
290+func PubkeyCommand() *cobra.Command {
291 cmd := &cobra.Command{
292 Use: "pubkey",
293 Aliases: []string{"pubkeys", "publickey", "publickeys"},
294@@ -64,7 +65,7 @@ func pubkeyCommand() *cobra.Command {
295 Aliases: []string{"ls"},
296 Short: "List public keys",
297 Args: cobra.NoArgs,
298- RunE: func(cmd *cobra.Command, args []string) error {
299+ RunE: func(cmd *cobra.Command, _ []string) error {
300 ctx := cmd.Context()
301 be := backend.FromContext(ctx)
302 pk := sshutils.PublicKeyFromContext(ctx)
303diff --git a/server/ssh/cmd/repo.go b/server/ssh/cmd/repo.go
304index fc23844fb1494268d6f352091617cdca6f3d8c44..0b5ff3e5db40e4f672349174a01ea05daa7fcae2 100644
305--- a/server/ssh/cmd/repo.go
306+++ b/server/ssh/cmd/repo.go
307@@ -9,7 +9,8 @@ import (
308 "github.com/spf13/cobra"
309 )
310
311-func repoCommand() *cobra.Command {
312+// RepoCommand returns a command for managing repositories.
313+func RepoCommand() *cobra.Command {
314 cmd := &cobra.Command{
315 Use: "repo",
316 Aliases: []string{"repos", "repository", "repositories"},
317diff --git a/server/ssh/cmd/set_username.go b/server/ssh/cmd/set_username.go
318index 71243ac5f5c39b7f2308473d64adec009639d53a..c3841f82ea7b480d36eb6f735e8bd0101014e92a 100644
319--- a/server/ssh/cmd/set_username.go
320+++ b/server/ssh/cmd/set_username.go
321@@ -6,7 +6,8 @@ import (
322 "github.com/spf13/cobra"
323 )
324
325-func setUsernameCommand() *cobra.Command {
326+// SetUsernameCommand returns a command that sets the user's username.
327+func SetUsernameCommand() *cobra.Command {
328 cmd := &cobra.Command{
329 Use: "set-username USERNAME",
330 Short: "Set your username",
331diff --git a/server/ssh/cmd/settings.go b/server/ssh/cmd/settings.go
332index 0fa4cace84aa42684d595f269706eea82bf39530..4131fb2b1b32353d4636505aa4263feed845eab0 100644
333--- a/server/ssh/cmd/settings.go
334+++ b/server/ssh/cmd/settings.go
335@@ -9,7 +9,8 @@ import (
336 "github.com/spf13/cobra"
337 )
338
339-func settingsCommand() *cobra.Command {
340+// SettingsCommand returns a command that manages server settings.
341+func SettingsCommand() *cobra.Command {
342 cmd := &cobra.Command{
343 Use: "settings",
344 Short: "Manage server settings",
345diff --git a/server/ssh/cmd/token.go b/server/ssh/cmd/token.go
346index cb3daad6fe8ef42e0b2bb1c463e45e9d03723bc8..3fefaa8cd6321b80a0cb5955f58334f46414b420 100644
347--- a/server/ssh/cmd/token.go
348+++ b/server/ssh/cmd/token.go
349@@ -13,7 +13,8 @@ import (
350 "github.com/spf13/cobra"
351 )
352
353-func tokenCommand() *cobra.Command {
354+// TokenCommand returns a command that manages user access tokens.
355+func TokenCommand() *cobra.Command {
356 cmd := &cobra.Command{
357 Use: "token",
358 Aliases: []string{"access-token"},
359diff --git a/server/ssh/cmd/user.go b/server/ssh/cmd/user.go
360index 639bf909adb773db202f7b95fae0aa8629b14425..f8b0a986d9fd135a8c242ca3e0dad9c972d59b13 100644
361--- a/server/ssh/cmd/user.go
362+++ b/server/ssh/cmd/user.go
363@@ -11,7 +11,8 @@ import (
364 "golang.org/x/crypto/ssh"
365 )
366
367-func userCommand() *cobra.Command {
368+// UserCommand returns the user subcommand.
369+func UserCommand() *cobra.Command {
370 cmd := &cobra.Command{
371 Use: "user",
372 Aliases: []string{"users"},
373diff --git a/server/ssh/git.go b/server/ssh/git.go
374deleted file mode 100644
375index b1a269a4999fe21181d327bf372f763701ca172b..0000000000000000000000000000000000000000
376--- a/server/ssh/git.go
377+++ /dev/null
378@@ -1,165 +0,0 @@
379-package ssh
380-
381-import (
382- "errors"
383- "path/filepath"
384- "time"
385-
386- "github.com/charmbracelet/log"
387- "github.com/charmbracelet/soft-serve/server/access"
388- "github.com/charmbracelet/soft-serve/server/backend"
389- "github.com/charmbracelet/soft-serve/server/config"
390- "github.com/charmbracelet/soft-serve/server/git"
391- "github.com/charmbracelet/soft-serve/server/lfs"
392- "github.com/charmbracelet/soft-serve/server/proto"
393- "github.com/charmbracelet/soft-serve/server/sshutils"
394- "github.com/charmbracelet/soft-serve/server/utils"
395- "github.com/charmbracelet/ssh"
396-)
397-
398-func handleGit(s ssh.Session) {
399- ctx := s.Context()
400- cfg := config.FromContext(ctx)
401- be := backend.FromContext(ctx)
402- logger := log.FromContext(ctx)
403- cmdLine := s.Command()
404- start := time.Now()
405-
406- // repo should be in the form of "repo.git"
407- name := utils.SanitizeRepo(cmdLine[1])
408- pk := s.PublicKey()
409- ak := sshutils.MarshalAuthorizedKey(pk)
410- user := proto.UserFromContext(ctx)
411- accessLevel := be.AccessLevelForUser(ctx, name, user)
412- // git bare repositories should end in ".git"
413- // https://git-scm.com/docs/gitrepository-layout
414- repoDir := name + ".git"
415- reposDir := filepath.Join(cfg.DataPath, "repos")
416- if err := git.EnsureWithin(reposDir, repoDir); err != nil {
417- sshFatal(s, err)
418- return
419- }
420-
421- // Set repo in context
422- repo, _ := be.Repository(ctx, name)
423- ctx.SetValue(proto.ContextKeyRepository, repo)
424-
425- // Environment variables to pass down to git hooks.
426- envs := []string{
427- "SOFT_SERVE_REPO_NAME=" + name,
428- "SOFT_SERVE_REPO_PATH=" + filepath.Join(reposDir, repoDir),
429- "SOFT_SERVE_PUBLIC_KEY=" + ak,
430- "SOFT_SERVE_LOG_PATH=" + filepath.Join(cfg.DataPath, "log", "hooks.log"),
431- }
432-
433- if user != nil {
434- envs = append(envs,
435- "SOFT_SERVE_USERNAME="+user.Username(),
436- )
437- }
438-
439- // Add ssh session & config environ
440- envs = append(envs, s.Environ()...)
441- envs = append(envs, cfg.Environ()...)
442-
443- repoPath := filepath.Join(reposDir, repoDir)
444- service := git.Service(cmdLine[0])
445- cmd := git.ServiceCommand{
446- Stdin: s,
447- Stdout: s,
448- Stderr: s.Stderr(),
449- Env: envs,
450- Dir: repoPath,
451- }
452-
453- logger.Debug("git middleware", "cmd", service, "access", accessLevel.String())
454-
455- switch service {
456- case git.ReceivePackService:
457- receivePackCounter.WithLabelValues(name).Inc()
458- defer func() {
459- receivePackSeconds.WithLabelValues(name).Add(time.Since(start).Seconds())
460- }()
461- if accessLevel < access.ReadWriteAccess {
462- sshFatal(s, git.ErrNotAuthed)
463- return
464- }
465- if repo == nil {
466- if _, err := be.CreateRepository(ctx, name, user, proto.RepositoryOptions{Private: false}); err != nil {
467- log.Errorf("failed to create repo: %s", err)
468- sshFatal(s, err)
469- return
470- }
471- createRepoCounter.WithLabelValues(name).Inc()
472- }
473-
474- if err := service.Handler(ctx, cmd); err != nil {
475- sshFatal(s, git.ErrSystemMalfunction)
476- }
477-
478diff --git a/server/ssh/logger.go b/server/ssh/logger.go
479deleted file mode 100644
480index c5b972f0b9b6237e56cfbddcc2e6fa897f59c13c..0000000000000000000000000000000000000000
481--- a/server/ssh/logger.go
482+++ /dev/null
483@@ -1,25 +0,0 @@
484-package ssh
485-
486-import "github.com/charmbracelet/log"
487-
488-type loggerAdapter struct {
489- *log.Logger
490- log.Level
491-}
492-
493-func (l *loggerAdapter) Printf(format string, args ...interface{}) {
494- switch l.Level {
495- case log.DebugLevel:
496- l.Logger.Debugf(format, args...)
497- case log.InfoLevel:
498- l.Logger.Infof(format, args...)
499- case log.WarnLevel:
500- l.Logger.Warnf(format, args...)
501- case log.ErrorLevel:
502- l.Logger.Errorf(format, args...)
503- case log.FatalLevel:
504- l.Logger.Fatalf(format, args...)
505- default:
506- l.Logger.Printf(format, args...)
507- }
508-}
509diff --git a/server/ssh/middleware.go b/server/ssh/middleware.go
510index 909c499d4ddffa5f2454c10185e7057e9283bb74..209ed1dd048a275c159125772d5a0a9ca2fea2b0 100644
511--- a/server/ssh/middleware.go
512+++ b/server/ssh/middleware.go
513@@ -1,20 +1,25 @@
514 package ssh
515
516 import (
517- "strings"
518-
519 "github.com/charmbracelet/log"
520 "github.com/charmbracelet/soft-serve/server/backend"
521 "github.com/charmbracelet/soft-serve/server/config"
522 "github.com/charmbracelet/soft-serve/server/db"
523+ "github.com/charmbracelet/soft-serve/server/proto"
524+ "github.com/charmbracelet/soft-serve/server/ssh/cmd"
525+ "github.com/charmbracelet/soft-serve/server/sshutils"
526 "github.com/charmbracelet/soft-serve/server/store"
527 "github.com/charmbracelet/ssh"
528+ "github.com/prometheus/client_golang/prometheus"
529+ "github.com/prometheus/client_golang/prometheus/promauto"
530+ "github.com/spf13/cobra"
531 )
532
533 // ContextMiddleware adds the config, backend, and logger to the session context.
534 func ContextMiddleware(cfg *config.Config, dbx *db.DB, datastore store.Store, be *backend.Backend, logger *log.Logger) func(ssh.Handler) ssh.Handler {
535 return func(sh ssh.Handler) ssh.Handler {
536 return func(s ssh.Session) {
537+ s.Context().SetValue(sshutils.ContextKeySession, s)
538 s.Context().SetValue(config.ContextKey, cfg)
539 s.Context().SetValue(db.ContextKey, dbx)
540 s.Context().SetValue(store.ContextKey, datastore)
541@@ -25,22 +30,89 @@ func ContextMiddleware(cfg *config.Config, dbx *db.DB, datastore store.Store, be
542 }
543 }
544
545+var cliCommandCounter = promauto.NewCounterVec(prometheus.CounterOpts{
546+ Namespace: "soft_serve",
547+ Subsystem: "cli",
548+ Name: "commands_total",
549+ Help: "Total times each command was called",
550+}, []string{"command"})
551+
552 // CommandMiddleware handles git commands and CLI commands.
553 // This middleware must be run after the ContextMiddleware.
554 func CommandMiddleware(sh ssh.Handler) ssh.Handler {
555 return func(s ssh.Session) {
556 func() {
557- cmdLine := s.Command()
558 _, _, ptyReq := s.Pty()
559 if ptyReq {
560 return
561 }
562
563- switch {
564- case len(cmdLine) >= 2 && strings.HasPrefix(cmdLine[0], "git-"):
565- handleGit(s)
566- default:
567- handleCli(s)
568+ ctx := s.Context()
569+ cfg := config.FromContext(ctx)
570+ logger := log.FromContext(ctx)
571+
572+ args := s.Command()
573+ cliCommandCounter.WithLabelValues(cmd.CommandName(args)).Inc()
574+ rootCmd := &cobra.Command{
575+ Short: "Soft Serve is a self-hostable Git server for the command line.",
576+ SilenceUsage: true,
577+ }
578+ rootCmd.CompletionOptions.DisableDefaultCmd = true
579+
580+ rootCmd.SetUsageTemplate(cmd.UsageTemplate)
581+ rootCmd.SetUsageFunc(cmd.UsageFunc)
582+ rootCmd.AddCommand(
583+ cmd.GitUploadPackCommand(),
584+ cmd.GitUploadArchiveCommand(),
585+ cmd.GitReceivePackCommand(),
586+ cmd.RepoCommand(),
587+ )
588+
589+ if cfg.LFS.Enabled {
590+ rootCmd.AddCommand(
591+ cmd.GitLFSAuthenticateCommand(),
592+ )
593+
594+ if cfg.LFS.SSHEnabled {
595+ rootCmd.AddCommand(
596+ cmd.GitLFSTransfer(),
597+ )
598+ }
599+ }
600+
601+ rootCmd.SetArgs(args)
602+ if len(args) == 0 {
603+ // otherwise it'll default to os.Args, which is not what we want.
604+ rootCmd.SetArgs([]string{"--help"})
605+ }
606+ rootCmd.SetIn(s)
607+ rootCmd.SetOut(s)
608+ rootCmd.SetErr(s.Stderr())
609+ rootCmd.SetContext(ctx)
610+
611+ user := proto.UserFromContext(ctx)
612+ isAdmin := cmd.IsPublicKeyAdmin(cfg, s.PublicKey()) || (user != nil && user.IsAdmin())
613diff --git a/server/ssh/ssh.go b/server/ssh/ssh.go
614index 297973ebf24d0be41b66fa4c8329310858ca4970..bd4fd4a899d2831886050a043056ba9779be2ae1 100644
615--- a/server/ssh/ssh.go
616+++ b/server/ssh/ssh.go
617@@ -13,7 +13,6 @@ import (
618 "github.com/charmbracelet/soft-serve/server/backend"
619 "github.com/charmbracelet/soft-serve/server/config"
620 "github.com/charmbracelet/soft-serve/server/db"
621- "github.com/charmbracelet/soft-serve/server/git"
622 "github.com/charmbracelet/soft-serve/server/proto"
623 "github.com/charmbracelet/soft-serve/server/store"
624 "github.com/charmbracelet/ssh"
625@@ -41,55 +40,6 @@ var (
626 Name: "keyboard_interactive_auth_total",
627 Help: "The total number of keyboard interactive auth requests",
628 }, []string{"allowed"})
629-
630- uploadPackCounter = promauto.NewCounterVec(prometheus.CounterOpts{
631- Namespace: "soft_serve",
632- Subsystem: "git",
633- Name: "upload_pack_total",
634- Help: "The total number of git-upload-pack requests",
635- }, []string{"repo"})
636-
637- receivePackCounter = promauto.NewCounterVec(prometheus.CounterOpts{
638- Namespace: "soft_serve",
639- Subsystem: "git",
640- Name: "receive_pack_total",
641- Help: "The total number of git-receive-pack requests",
642- }, []string{"repo"})
643-
644- uploadArchiveCounter = promauto.NewCounterVec(prometheus.CounterOpts{
645- Namespace: "soft_serve",
646- Subsystem: "git",
647- Name: "upload_archive_total",
648- Help: "The total number of git-upload-archive requests",
649- }, []string{"repo"})
650-
651- uploadPackSeconds = promauto.NewCounterVec(prometheus.CounterOpts{
652- Namespace: "soft_serve",
653- Subsystem: "git",
654- Name: "upload_pack_seconds_total",
655- Help: "The total time spent on git-upload-pack requests",
656- }, []string{"repo"})
657-
658- receivePackSeconds = promauto.NewCounterVec(prometheus.CounterOpts{
659- Namespace: "soft_serve",
660- Subsystem: "git",
661- Name: "receive_pack_seconds_total",
662- Help: "The total time spent on git-receive-pack requests",
663- }, []string{"repo"})
664-
665- uploadArchiveSeconds = promauto.NewCounterVec(prometheus.CounterOpts{
666- Namespace: "soft_serve",
667- Subsystem: "git",
668- Name: "upload_archive_seconds_total",
669- Help: "The total time spent on git-upload-archive requests",
670- }, []string{"repo"})
671-
672- createRepoCounter = promauto.NewCounterVec(prometheus.CounterOpts{
673- Namespace: "soft_serve",
674- Subsystem: "ssh",
675- Name: "create_repo_total",
676- Help: "The total number of create repo requests",
677- }, []string{"repo"})
678 )
679
680 // SSHServer is a SSH server that implements the git protocol.
681@@ -209,9 +159,3 @@ func (s *SSHServer) KeyboardInteractiveHandler(ctx ssh.Context, _ gossh.Keyboard
682 keyboardInteractiveCounter.WithLabelValues(strconv.FormatBool(ac)).Inc()
683 return ac
684 }
685-
686-// sshFatal prints to the session's STDOUT as a git response and exit 1.
687-func sshFatal(s ssh.Session, err error) {
688- git.WritePktlineErr(s, err) // nolint: errcheck
689- s.Exit(1) // nolint: errcheck
690-}
691diff --git a/server/sshutils/utils.go b/server/sshutils/utils.go
692index 6bba64578adff81c72bd9f8bb89eee2a3887b6ad..2686ac7bc306e4cdbf40687087d1ed15375c0b5c 100644
693--- a/server/sshutils/utils.go
694+++ b/server/sshutils/utils.go
695@@ -38,3 +38,14 @@ func PublicKeyFromContext(ctx context.Context) gossh.PublicKey {
696 }
697 return nil
698 }
699+
700+// ContextKeySession is the context key for the SSH session.
701+var ContextKeySession = &struct{ string }{"session"}
702+
703+// SessionFromContext returns the SSH session from the context.
704+func SessionFromContext(ctx context.Context) ssh.Session {
705+ if s, ok := ctx.Value(ContextKeySession).(ssh.Session); ok {
706+ return s
707+ }
708+ return nil
709+}
710diff --git a/server/web/auth.go b/server/web/auth.go
711index fb090fa6311e73cc816e7c3bd024834b9be431f6..bbbe88b3f915479aedce60b58ff68bab248b7402 100644
712--- a/server/web/auth.go
713+++ b/server/web/auth.go
714@@ -51,7 +51,7 @@ func parseUsernamePassword(ctx context.Context, username, password string) (prot
715 return nil, ErrInvalidPassword
716 } else if username != "" {
717 // Try to authenticate using access token as the username
718- logger.Info("trying to authenticate using access token as username", "username", username)
719+ logger.Debug("trying to authenticate using access token as username", "username", username)
720 user, err := be.UserByAccessToken(ctx, username)
721 if err == nil {
722 return user, nil
723diff --git a/server/web/git.go b/server/web/git.go
724index d2d640c5f730b0d4eec28c4cf09d6dcbc0e44f52..121b85795d2e4a0dfa7aa2dafd6f6e0c8990b5ea 100644
725--- a/server/web/git.go
726+++ b/server/web/git.go
727@@ -94,7 +94,6 @@ func withParams(h http.Handler) http.Handler {
728 repo = utils.SanitizeRepo(repo)
729 vars["repo"] = repo
730 vars["dir"] = filepath.Join(cfg.DataPath, "repos", repo+".git")
731- logger.Info("request vars", "vars", vars)
732
733 // Add repo suffix (.git)
734 r.URL.Path = fmt.Sprintf("%s.git/%s", repo, vars["file"])
735@@ -233,7 +232,7 @@ func withAccess(next http.Handler) http.HandlerFunc {
736 r = r.WithContext(ctx)
737
738 if user != nil {
739- logger.Info("found user", "username", user.Username())
740+ logger.Debug("authenticated", "username", user.Username())
741 }
742
743 service := git.Service(mux.Vars(r)["service"])
744diff --git a/testscript/testdata/help.txtar b/testscript/testdata/help.txtar
745index 58868b7cc750df0c591117f608978605b38e3da4..257ad244799114f510f9c470075575d8d0ba411d 100644
746--- a/testscript/testdata/help.txtar
747+++ b/testscript/testdata/help.txtar
748@@ -11,15 +11,15 @@ Usage:
749 ssh -p $SSH_PORT localhost [command]
750
751 Available Commands:
752- help Help about any command
753- info Show your info
754- jwt Generate a JSON Web Token
755- pubkey Manage your public keys
756- repo Manage repositories
757- set-username Set your username
758- settings Manage server settings
759- token Manage access tokens
760- user Manage users
761+ help Help about any command
762+ info Show your info
763+ jwt Generate a JSON Web Token
764+ pubkey Manage your public keys
765+ repo Manage repositories
766+ set-username Set your username
767+ settings Manage server settings
768+ token Manage access tokens
769+ user Manage users
770
771 Flags:
772 -h, --help help for this command
773diff --git a/testscript/testdata/ssh.txtar b/testscript/testdata/ssh.txtar
774new file mode 100644
775index 0000000000000000000000000000000000000000..b4e2237399232e91394bb0d26b172c0d166b0ad6
776--- /dev/null
777+++ b/testscript/testdata/ssh.txtar
778@@ -0,0 +1,99 @@
779+# vi: set ft=conf
780+
781+[windows] dos2unix argserr1.txt argserr2.txt argserr3.txt invalidrepoerr.txt notauthorizederr.txt
782+
783+# create a user
784+soft user create foo --key "$USER1_AUTHORIZED_KEY"
785+
786+# create a repo
787+soft repo create repo1
788+soft repo create repo1p -p
789+usoft repo create repo2
790+usoft repo create repo2p -p
791+
792+# SSH Git commands as admin
793+! soft git-upload-pack
794+cmp stderr argserr1.txt
795+! soft git-upload-pack foobar
796+cmp stderr invalidrepoerr.txt
797+! soft git-upload-archive
798+cmp stderr argserr1.txt
799+! soft git-upload-archive foobar
800+cmp stderr invalidrepoerr.txt
801+! soft git-receive-pack
802+cmp stderr argserr1.txt
803+! soft git-receive-pack foobar
804+stdout '.*0000 capabilities.*git.*' # git pack response
805+stderr '.*something went wrong.*'
806+! soft git-lfs-authenticate
807+cmp stderr argserr2.txt
808+! soft git-lfs-authenticate foobar
809+cmp stderr argserr3.txt
810+! soft git-lfs-authenticate foobar download
811+cmp stderr invalidrepoerr.txt
812+! soft git-lfs-authenticate foobar upload
813+cmp stderr invalidrepoerr.txt
814+soft git-lfs-authenticate repo1 download
815+stdout '.*header.*Bearer.*href.*expires_in.*expires_at.*'
816+soft git-lfs-authenticate repo1 upload
817+stdout '.*header.*Bearer.*href.*expires_in.*expires_at.*'
818+soft git-lfs-authenticate repo1p download
819+stdout '.*header.*Bearer.*href.*expires_in.*expires_at.*'
820+soft git-lfs-authenticate repo1p upload
821+stdout '.*header.*Bearer.*href.*expires_in.*expires_at.*'
822+soft git-lfs-authenticate repo2 download
823+stdout '.*header.*Bearer.*href.*expires_in.*expires_at.*'
824+soft git-lfs-authenticate repo2 upload
825+stdout '.*header.*Bearer.*href.*expires_in.*expires_at.*'
826+soft git-lfs-authenticate repo2p download
827+stdout '.*header.*Bearer.*href.*expires_in.*expires_at.*'
828+soft git-lfs-authenticate repo2p upload
829+stdout '.*header.*Bearer.*href.*expires_in.*expires_at.*'
830+
831+# SSH Git commands as user
832+! usoft git-upload-pack
833+cmp stderr argserr1.txt
834+! usoft git-upload-pack foobar
835+cmp stderr invalidrepoerr.txt
836+! usoft git-upload-archive
837+cmp stderr argserr1.txt
838+! usoft git-upload-archive foobar
839+cmp stderr invalidrepoerr.txt
840+! usoft git-receive-pack
841+cmp stderr argserr1.txt
842+! usoft git-receive-pack foobar
843+stdout '.*0000 capabilities.*git.*' # git pack response
844+stderr '.*something went wrong.*'
845+! usoft git-lfs-authenticate
846+cmp stderr argserr2.txt
847+! usoft git-lfs-authenticate foobar download
848+cmp stderr invalidrepoerr.txt
849+! usoft git-lfs-authenticate foobar upload
850+cmp stderr invalidrepoerr.txt
851+usoft git-lfs-authenticate repo1 download
852+stdout '.*header.*Bearer.*href.*expires_in.*expires_at.*'
853+! usoft git-lfs-authenticate repo1 upload
854+cmp stderr notauthorizederr.txt
855+! usoft git-lfs-authenticate repo1p download
856+cmp stderr notauthorizederr.txt
857+! usoft git-lfs-authenticate repo1p upload
858+cmp stderr notauthorizederr.txt
859+usoft git-lfs-authenticate repo2 download
860+stdout '.*header.*Bearer.*href.*expires_in.*expires_at.*'
861+usoft git-lfs-authenticate repo2 upload
862+stdout '.*header.*Bearer.*href.*expires_in.*expires_at.*'
863+usoft git-lfs-authenticate repo2p download
864+stdout '.*header.*Bearer.*href.*expires_in.*expires_at.*'
865+usoft git-lfs-authenticate repo2p upload
866+stdout '.*header.*Bearer.*href.*expires_in.*expires_at.*'
867+
868+-- argserr1.txt --
869+Error: accepts 1 arg(s), received 0
870+-- argserr2.txt --
871+Error: accepts 2 arg(s), received 0
872+-- argserr3.txt --
873+Error: accepts 2 arg(s), received 1
874+-- invalidrepoerr.txt --
875+Error: invalid repo
876+-- notauthorizederr.txt --
877+Error: you are not authorized to do this