1package cmd
  2
  3import (
  4	"bufio"
  5	"fmt"
  6	"strings"
  7
  8	"github.com/charmbracelet/keygen"
  9	"github.com/charmbracelet/soft-serve/server/hooks"
 10	"github.com/charmbracelet/ssh"
 11	"github.com/spf13/cobra"
 12)
 13
 14// hookCommand handles Soft Serve internal API git hook requests.
 15func hookCommand() *cobra.Command {
 16	preReceiveCmd := &cobra.Command{
 17		Use:               "pre-receive",
 18		Short:             "Run git pre-receive hook",
 19		PersistentPreRunE: checkIfInternal,
 20		RunE: func(cmd *cobra.Command, args []string) error {
 21			_, s := fromContext(cmd)
 22			hks := cmd.Context().Value(HooksCtxKey).(hooks.Hooks)
 23			repoName := getRepoName(s)
 24			opts := make([]hooks.HookArg, 0)
 25			scanner := bufio.NewScanner(s)
 26			for scanner.Scan() {
 27				fields := strings.Fields(scanner.Text())
 28				if len(fields) != 3 {
 29					return fmt.Errorf("invalid pre-receive hook input: %s", scanner.Text())
 30				}
 31				opts = append(opts, hooks.HookArg{
 32					OldSha:  fields[0],
 33					NewSha:  fields[1],
 34					RefName: fields[2],
 35				})
 36			}
 37			hks.PreReceive(s, s.Stderr(), repoName, opts)
 38			return nil
 39		},
 40	}
 41
 42	updateCmd := &cobra.Command{
 43		Use:               "update",
 44		Short:             "Run git update hook",
 45		Args:              cobra.ExactArgs(3),
 46		PersistentPreRunE: checkIfInternal,
 47		RunE: func(cmd *cobra.Command, args []string) error {
 48			_, s := fromContext(cmd)
 49			hks := cmd.Context().Value(HooksCtxKey).(hooks.Hooks)
 50			repoName := getRepoName(s)
 51			hks.Update(s, s.Stderr(), repoName, hooks.HookArg{
 52				RefName: args[0],
 53				OldSha:  args[1],
 54				NewSha:  args[2],
 55			})
 56			return nil
 57		},
 58	}
 59
 60	postReceiveCmd := &cobra.Command{
 61		Use:               "post-receive",
 62		Short:             "Run git post-receive hook",
 63		PersistentPreRunE: checkIfInternal,
 64		RunE: func(cmd *cobra.Command, _ []string) error {
 65			_, s := fromContext(cmd)
 66			hks := cmd.Context().Value(HooksCtxKey).(hooks.Hooks)
 67			repoName := getRepoName(s)
 68			opts := make([]hooks.HookArg, 0)
 69			scanner := bufio.NewScanner(s)
 70			for scanner.Scan() {
 71				fields := strings.Fields(scanner.Text())
 72				if len(fields) != 3 {
 73					return fmt.Errorf("invalid post-receive hook input: %s", scanner.Text())
 74				}
 75				opts = append(opts, hooks.HookArg{
 76					OldSha:  fields[0],
 77					NewSha:  fields[1],
 78					RefName: fields[2],
 79				})
 80			}
 81			hks.PostReceive(s, s.Stderr(), repoName, opts)
 82			return nil
 83		},
 84	}
 85
 86	postUpdateCmd := &cobra.Command{
 87		Use:               "post-update",
 88		Short:             "Run git post-update hook",
 89		PersistentPreRunE: checkIfInternal,
 90		RunE: func(cmd *cobra.Command, args []string) error {
 91			_, s := fromContext(cmd)
 92			hks := cmd.Context().Value(HooksCtxKey).(hooks.Hooks)
 93			repoName := getRepoName(s)
 94			hks.PostUpdate(s, s.Stderr(), repoName, args...)
 95			return nil
 96		},
 97	}
 98
 99	hookCmd := &cobra.Command{
100		Use:          "hook",
101		Short:        "Run git server hooks",
102		Hidden:       true,
103		SilenceUsage: true,
104	}
105
106	hookCmd.AddCommand(
107		preReceiveCmd,
108		updateCmd,
109		postReceiveCmd,
110		postUpdateCmd,
111	)
112
113	return hookCmd
114}
115
116// Check if the session's public key matches the internal API key.
117func checkIfInternal(cmd *cobra.Command, _ []string) error {
118	cfg, s := fromContext(cmd)
119	pk := s.PublicKey()
120	kp, err := keygen.New(cfg.SSH.InternalKeyPath, keygen.WithKeyType(keygen.Ed25519))
121	if err != nil {
122		logger.Errorf("failed to read internal key: %v", err)
123		return err
124	}
125	if !ssh.KeysEqual(pk, kp.PublicKey()) {
126		return ErrUnauthorized
127	}
128	return nil
129}
130
131func getRepoName(s ssh.Session) string {
132	var repoName string
133	for _, env := range s.Environ() {
134		if strings.HasPrefix(env, "SOFT_SERVE_REPO_NAME=") {
135			return strings.TrimPrefix(env, "SOFT_SERVE_REPO_NAME=")
136		}
137	}
138	return repoName
139}