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		blobCommand(),
 59		tagCommand(),
 60		treeCommand(),
 61	)
 62
 63	return rootCmd
 64}
 65
 66func fromContext(cmd *cobra.Command) (*config.Config, ssh.Session) {
 67	ctx := cmd.Context()
 68	cfg := ctx.Value(ConfigCtxKey).(*config.Config)
 69	s := ctx.Value(SessionCtxKey).(ssh.Session)
 70	return cfg, s
 71}
 72
 73func checkIfReadable(cmd *cobra.Command, args []string) error {
 74	var repo string
 75	if len(args) > 0 {
 76		repo = args[0]
 77	}
 78	cfg, s := fromContext(cmd)
 79	rn := utils.SanitizeRepo(repo)
 80	auth := cfg.Backend.AccessLevel(rn, s.PublicKey())
 81	if auth < backend.ReadOnlyAccess {
 82		return ErrUnauthorized
 83	}
 84	return nil
 85}
 86
 87func checkIfAdmin(cmd *cobra.Command, args []string) error {
 88	cfg, s := fromContext(cmd)
 89	if !cfg.Backend.IsAdmin(s.PublicKey()) {
 90		return ErrUnauthorized
 91	}
 92	return nil
 93}
 94
 95func checkIfCollab(cmd *cobra.Command, args []string) error {
 96	var repo string
 97	if len(args) > 0 {
 98		repo = args[0]
 99	}
100	cfg, s := fromContext(cmd)
101	rn := utils.SanitizeRepo(repo)
102	auth := cfg.Backend.AccessLevel(rn, s.PublicKey())
103	if auth < backend.ReadWriteAccess {
104		return ErrUnauthorized
105	}
106	return nil
107}
108
109// Middleware is the Soft Serve middleware that handles SSH commands.
110func Middleware(cfg *config.Config) wish.Middleware {
111	return func(sh ssh.Handler) ssh.Handler {
112		return func(s ssh.Session) {
113			func() {
114				_, _, active := s.Pty()
115				if active {
116					return
117				}
118
119				// Ignore git server commands.
120				args := s.Command()
121				if len(args) > 0 {
122					if args[0] == "git-receive-pack" ||
123						args[0] == "git-upload-pack" ||
124						args[0] == "git-upload-archive" {
125						return
126					}
127				}
128
129				ctx := context.WithValue(s.Context(), ConfigCtxKey, cfg)
130				ctx = context.WithValue(ctx, SessionCtxKey, s)
131
132				rootCmd := rootCommand()
133				rootCmd.SetArgs(args)
134				if len(args) == 0 {
135					// otherwise it'll default to os.Args, which is not what we want.
136					rootCmd.SetArgs([]string{"--help"})
137				}
138				rootCmd.SetIn(s)
139				rootCmd.SetOut(s)
140				rootCmd.CompletionOptions.DisableDefaultCmd = true
141				rootCmd.SetErr(s.Stderr())
142				if err := rootCmd.ExecuteContext(ctx); err != nil {
143					_ = s.Exit(1)
144				}
145			}()
146			sh(s)
147		}
148	}
149}