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