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}