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}