hook.go

  1package main
  2
  3import (
  4	"fmt"
  5	"os"
  6	"path/filepath"
  7	"strings"
  8
  9	"github.com/charmbracelet/keygen"
 10	"github.com/charmbracelet/soft-serve/server/config"
 11	"github.com/spf13/cobra"
 12	gossh "golang.org/x/crypto/ssh"
 13)
 14
 15var (
 16	configPath string
 17
 18	hookCmd = &cobra.Command{
 19		Use:    "hook",
 20		Short:  "Run git server hooks",
 21		Long:   "Handles Soft Serve git server hooks.",
 22		Hidden: true,
 23		RunE: func(_ *cobra.Command, args []string) error {
 24			c, s, err := commonInit()
 25			if err != nil {
 26				return err
 27			}
 28			defer c.Close() //nolint:errcheck
 29			defer s.Close() //nolint:errcheck
 30			s.Stdin = os.Stdin
 31			s.Stdout = os.Stdout
 32			s.Stderr = os.Stderr
 33			cmd := fmt.Sprintf("hook %s", strings.Join(args, " "))
 34			if err := s.Run(cmd); err != nil {
 35				return err
 36			}
 37			return nil
 38		},
 39	}
 40)
 41
 42func init() {
 43	hookCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "path to config file")
 44}
 45
 46// TODO: use ssh controlmaster
 47func commonInit() (c *gossh.Client, s *gossh.Session, err error) {
 48	cfg, err := config.ParseConfig(configPath)
 49	if err != nil {
 50		return
 51	}
 52
 53	// Git runs the hook within the repository's directory.
 54	// Get the working directory to determine the repository name.
 55	wd, err := os.Getwd()
 56	if err != nil {
 57		return
 58	}
 59
 60	rs, err := filepath.Abs(filepath.Join(cfg.DataPath, "repos"))
 61	if err != nil {
 62		return
 63	}
 64
 65	if !strings.HasPrefix(wd, rs) {
 66		err = fmt.Errorf("hook must be run from within repository directory")
 67		return
 68	}
 69	repoName := strings.TrimPrefix(wd, rs)
 70	repoName = strings.TrimPrefix(repoName, string(os.PathSeparator))
 71	c, err = newClient(cfg)
 72	if err != nil {
 73		return
 74	}
 75	s, err = newSession(c)
 76	if err != nil {
 77		return
 78	}
 79	s.Setenv("SOFT_SERVE_REPO_NAME", repoName)
 80	return
 81}
 82
 83func newClient(cfg *config.Config) (*gossh.Client, error) {
 84	// Only accept the server's host key.
 85	pk, err := keygen.New(cfg.Internal.KeyPath, keygen.WithKeyType(keygen.Ed25519))
 86	if err != nil {
 87		return nil, err
 88	}
 89	ik, err := keygen.New(cfg.Internal.InternalKeyPath, keygen.WithKeyType(keygen.Ed25519))
 90	if err != nil {
 91		return nil, err
 92	}
 93	cc := &gossh.ClientConfig{
 94		User: "internal",
 95		Auth: []gossh.AuthMethod{
 96			gossh.PublicKeys(ik.Signer()),
 97		},
 98		HostKeyCallback: gossh.FixedHostKey(pk.PublicKey()),
 99	}
100	c, err := gossh.Dial("tcp", cfg.Internal.ListenAddr, cc)
101	if err != nil {
102		return nil, err
103	}
104	return c, nil
105}
106
107func newSession(c *gossh.Client) (*gossh.Session, error) {
108	s, err := c.NewSession()
109	if err != nil {
110		return nil, err
111	}
112	return s, nil
113}