1package cmd
  2
  3import (
  4	"context"
  5	"fmt"
  6
  7	"github.com/charmbracelet/soft-serve/server/backend"
  8	"github.com/charmbracelet/soft-serve/server/config"
  9	"github.com/charmbracelet/soft-serve/server/utils"
 10	"github.com/charmbracelet/ssh"
 11	"github.com/charmbracelet/wish"
 12	"github.com/spf13/cobra"
 13)
 14
 15// ContextKey is a type that can be used as a key in a context.
 16type ContextKey string
 17
 18// String returns the string representation of the ContextKey.
 19func (c ContextKey) String() string {
 20	return string(c) + "ContextKey"
 21}
 22
 23var (
 24	// ConfigCtxKey is the key for the config in the context.
 25	ConfigCtxKey = ContextKey("config")
 26	// SessionCtxKey is the key for the session in the context.
 27	SessionCtxKey = ContextKey("session")
 28)
 29
 30var (
 31	// ErrUnauthorized is returned when the user is not authorized to perform action.
 32	ErrUnauthorized = fmt.Errorf("Unauthorized")
 33	// ErrRepoNotFound is returned when the repo is not found.
 34	ErrRepoNotFound = fmt.Errorf("Repository not found")
 35	// ErrFileNotFound is returned when the file is not found.
 36	ErrFileNotFound = fmt.Errorf("File not found")
 37)
 38
 39// rootCommand is the root command for the server.
 40func rootCommand() *cobra.Command {
 41	rootCmd := &cobra.Command{
 42		Use:          "soft",
 43		Short:        "Soft Serve is a self-hostable Git server for the command line.",
 44		SilenceUsage: true,
 45	}
 46	// TODO: use command usage template to include hostname and port
 47	rootCmd.CompletionOptions.DisableDefaultCmd = true
 48	rootCmd.AddCommand(
 49		adminCommand(),
 50		branchCommand(),
 51		collabCommand(),
 52		createCommand(),
 53		deleteCommand(),
 54		descriptionCommand(),
 55		listCommand(),
 56		privateCommand(),
 57		renameCommand(),
 58		showCommand(),
 59		tagCommand(),
 60	)
 61
 62	return rootCmd
 63}
 64
 65func fromContext(cmd *cobra.Command) (*config.Config, ssh.Session) {
 66	ctx := cmd.Context()
 67	cfg := ctx.Value(ConfigCtxKey).(*config.Config)
 68	s := ctx.Value(SessionCtxKey).(ssh.Session)
 69	return cfg, s
 70}
 71
 72func checkIfReadable(cmd *cobra.Command, args []string) error {
 73	var repo string
 74	if len(args) > 0 {
 75		repo = args[0]
 76	}
 77	cfg, s := fromContext(cmd)
 78	rn := utils.SanitizeRepo(repo)
 79	auth := cfg.Backend.AccessLevel(rn, s.PublicKey())
 80	if auth < backend.ReadOnlyAccess {
 81		return ErrUnauthorized
 82	}
 83	return nil
 84}
 85
 86func checkIfAdmin(cmd *cobra.Command, args []string) error {
 87	cfg, s := fromContext(cmd)
 88	if !cfg.Backend.IsAdmin(s.PublicKey()) {
 89		return ErrUnauthorized
 90	}
 91	return nil
 92}
 93
 94func checkIfCollab(cmd *cobra.Command, args []string) error {
 95	var repo string
 96	if len(args) > 0 {
 97		repo = args[0]
 98	}
 99	cfg, s := fromContext(cmd)
100	rn := utils.SanitizeRepo(repo)
101	auth := cfg.Backend.AccessLevel(rn, s.PublicKey())
102	if auth < backend.ReadWriteAccess {
103		return ErrUnauthorized
104	}
105	return nil
106}
107
108// Middleware is the Soft Serve middleware that handles SSH commands.
109func Middleware(cfg *config.Config) wish.Middleware {
110	return func(sh ssh.Handler) ssh.Handler {
111		return func(s ssh.Session) {
112			func() {
113				_, _, active := s.Pty()
114				if active {
115					return
116				}
117
118				// Ignore git server commands.
119				args := s.Command()
120				if len(args) > 0 {
121					if args[0] == "git-receive-pack" ||
122						args[0] == "git-upload-pack" ||
123						args[0] == "git-upload-archive" {
124						return
125					}
126				}
127
128				ctx := context.WithValue(s.Context(), ConfigCtxKey, cfg)
129				ctx = context.WithValue(ctx, SessionCtxKey, s)
130
131				rootCmd := rootCommand()
132				rootCmd.SetArgs(args)
133				if len(args) == 0 {
134					// otherwise it'll default to os.Args, which is not what we want.
135					rootCmd.SetArgs([]string{"--help"})
136				}
137				rootCmd.SetIn(s)
138				rootCmd.SetOut(s)
139				rootCmd.CompletionOptions.DisableDefaultCmd = true
140				rootCmd.SetErr(s.Stderr())
141				if err := rootCmd.ExecuteContext(ctx); err != nil {
142					_ = s.Exit(1)
143				}
144			}()
145			sh(s)
146		}
147	}
148}