cmd.go

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