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}