internal.go

  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}