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(cfg *config.Config, s ssh.Session) *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
 55	// TODO: use command usage template to include hostname and port
 56	rootCmd.CompletionOptions.DisableDefaultCmd = true
 57	rootCmd.AddCommand(
 58		hookCommand(),
 59		repoCommand(),
 60	)
 61
 62	user, _ := cfg.Backend.UserByPublicKey(s.PublicKey())
 63	if user != nil {
 64		if user.IsAdmin() {
 65			rootCmd.AddCommand(
 66				settingsCommand(),
 67				userCommand(),
 68			)
 69		}
 70
 71		rootCmd.AddCommand(
 72			infoCommand(),
 73			pubkeyCommand(),
 74			setUsernameCommand(),
 75		)
 76	}
 77
 78	return rootCmd
 79}
 80
 81func fromContext(cmd *cobra.Command) (*config.Config, ssh.Session) {
 82	ctx := cmd.Context()
 83	cfg := ctx.Value(ConfigCtxKey).(*config.Config)
 84	s := ctx.Value(SessionCtxKey).(ssh.Session)
 85	return cfg, s
 86}
 87
 88func checkIfReadable(cmd *cobra.Command, args []string) error {
 89	var repo string
 90	if len(args) > 0 {
 91		repo = args[0]
 92	}
 93	cfg, s := fromContext(cmd)
 94	rn := utils.SanitizeRepo(repo)
 95	auth := cfg.Backend.AccessLevelByPublicKey(rn, s.PublicKey())
 96	if auth < backend.ReadOnlyAccess {
 97		return ErrUnauthorized
 98	}
 99	return nil
100}
101
102func checkIfAdmin(cmd *cobra.Command, _ []string) error {
103	cfg, s := fromContext(cmd)
104	ak := backend.MarshalAuthorizedKey(s.PublicKey())
105	for _, k := range cfg.InitialAdminKeys {
106		if k == ak {
107			return nil
108		}
109	}
110
111	user, err := cfg.Backend.UserByPublicKey(s.PublicKey())
112	if err != nil {
113		return err
114	}
115
116	if !user.IsAdmin() {
117		return ErrUnauthorized
118	}
119
120	return nil
121}
122
123func checkIfCollab(cmd *cobra.Command, args []string) error {
124	var repo string
125	if len(args) > 0 {
126		repo = args[0]
127	}
128	cfg, s := fromContext(cmd)
129	rn := utils.SanitizeRepo(repo)
130	auth := cfg.Backend.AccessLevelByPublicKey(rn, s.PublicKey())
131	if auth < backend.ReadWriteAccess {
132		return ErrUnauthorized
133	}
134	return nil
135}
136
137// Middleware is the Soft Serve middleware that handles SSH commands.
138func Middleware(cfg *config.Config, hooks hooks.Hooks) wish.Middleware {
139	return func(sh ssh.Handler) ssh.Handler {
140		return func(s ssh.Session) {
141			func() {
142				_, _, active := s.Pty()
143				if active {
144					return
145				}
146
147				// Ignore git server commands.
148				args := s.Command()
149				if len(args) > 0 {
150					if args[0] == "git-receive-pack" ||
151						args[0] == "git-upload-pack" ||
152						args[0] == "git-upload-archive" {
153						return
154					}
155				}
156
157				ctx := context.WithValue(s.Context(), ConfigCtxKey, cfg)
158				ctx = context.WithValue(ctx, SessionCtxKey, s)
159				ctx = context.WithValue(ctx, HooksCtxKey, hooks)
160
161				rootCmd := rootCommand(cfg, s)
162				rootCmd.SetArgs(args)
163				if len(args) == 0 {
164					// otherwise it'll default to os.Args, which is not what we want.
165					rootCmd.SetArgs([]string{"--help"})
166				}
167				rootCmd.SetIn(s)
168				rootCmd.SetOut(s)
169				rootCmd.CompletionOptions.DisableDefaultCmd = true
170				rootCmd.SetErr(s.Stderr())
171				if err := rootCmd.ExecuteContext(ctx); err != nil {
172					_ = s.Exit(1)
173				}
174			}()
175			sh(s)
176		}
177	}
178}