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