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