diff --git a/cmd/soft/hook.go b/cmd/soft/hook.go index de16e3e89e0c14e8e040d7b1140375ca4f04dd4a..a24fc6b33c20ee7e1e83147beb6112f7f0b368a2 100644 --- a/cmd/soft/hook.go +++ b/cmd/soft/hook.go @@ -1,15 +1,23 @@ package main import ( + "bufio" + "bytes" + "context" "fmt" "os" - "path/filepath" "strings" - "github.com/charmbracelet/keygen" + "github.com/charmbracelet/soft-serve/server/backend" + "github.com/charmbracelet/soft-serve/server/backend/sqlite" "github.com/charmbracelet/soft-serve/server/config" + "github.com/charmbracelet/soft-serve/server/hooks" "github.com/spf13/cobra" - gossh "golang.org/x/crypto/ssh" +) + +var ( + confixCtxKey = "config" + backendCtxKey = "backend" ) var ( @@ -20,94 +28,113 @@ var ( Short: "Run git server hooks", Long: "Handles Soft Serve git server hooks.", Hidden: true, - RunE: func(_ *cobra.Command, args []string) error { - c, s, err := commonInit() + PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { + cfg, err := config.ParseConfig(configPath) if err != nil { - return err + return fmt.Errorf("could not parse config: %w", err) } - defer c.Close() //nolint:errcheck - defer s.Close() //nolint:errcheck - s.Stdin = os.Stdin - s.Stdout = os.Stdout - s.Stderr = os.Stderr - cmd := fmt.Sprintf("hook %s", strings.Join(args, " ")) - if err := s.Run(cmd); err != nil { - return err + + // Set up the backend + // TODO: support other backends + sb, err := sqlite.NewSqliteBackend(cmd.Context(), cfg) + if err != nil { + return fmt.Errorf("failed to create sqlite backend: %w", err) } + + cfg = cfg.WithBackend(sb) + + cmd.SetContext(context.WithValue(cmd.Context(), confixCtxKey, cfg)) + cmd.SetContext(context.WithValue(cmd.Context(), backendCtxKey, sb)) + return nil }, } -) -func init() { - hookCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "path to config file") -} + hooksRunE = func(cmd *cobra.Command, args []string) error { + cfg := cmd.Context().Value(confixCtxKey).(*config.Config) + hks := cfg.Backend.(backend.Hooks) -// TODO: use ssh controlmaster -func commonInit() (c *gossh.Client, s *gossh.Session, err error) { - cfg, err := config.ParseConfig(configPath) - if err != nil { - return - } + // This is set in the server before invoking git-receive-pack/git-upload-pack + repoName := os.Getenv("SOFT_SERVE_REPO_NAME") - // Git runs the hook within the repository's directory. - // Get the working directory to determine the repository name. - wd, err := os.Getwd() - if err != nil { - return - } + in := cmd.InOrStdin() + out := cmd.OutOrStdout() + err := cmd.ErrOrStderr() - rs, err := filepath.Abs(filepath.Join(cfg.DataPath, "repos")) - if err != nil { - return - } + cmdName := cmd.Name() + switch cmdName { + case hooks.PreReceiveHook, hooks.PostReceiveHook: + var buf bytes.Buffer + opts := make([]backend.HookArg, 0) + scanner := bufio.NewScanner(in) + for scanner.Scan() { + buf.Write(scanner.Bytes()) + fields := strings.Fields(scanner.Text()) + if len(fields) != 3 { + return fmt.Errorf("invalid pre-receive hook input: %s", scanner.Text()) + } + opts = append(opts, backend.HookArg{ + OldSha: fields[0], + NewSha: fields[1], + RefName: fields[2], + }) + } - if !strings.HasPrefix(wd, rs) { - err = fmt.Errorf("hook must be run from within repository directory") - return - } - repoName := strings.TrimPrefix(wd, rs) - repoName = strings.TrimPrefix(repoName, string(os.PathSeparator)) - c, err = newClient(cfg) - if err != nil { - return - } - s, err = newSession(c) - if err != nil { - return - } - s.Setenv("SOFT_SERVE_REPO_NAME", repoName) - return -} + switch cmdName { + case hooks.PreReceiveHook: + hks.PreReceive(out, err, repoName, opts) + case hooks.PostReceiveHook: + hks.PostReceive(out, err, repoName, opts) + } + case hooks.UpdateHook: + if len(args) != 3 { + return fmt.Errorf("invalid update hook input: %s", args) + } -func newClient(cfg *config.Config) (*gossh.Client, error) { - // Only accept the server's host key. - pk, err := keygen.New(cfg.Internal.KeyPath, keygen.WithKeyType(keygen.Ed25519)) - if err != nil { - return nil, err + hks.Update(out, err, repoName, backend.HookArg{ + OldSha: args[0], + NewSha: args[1], + RefName: args[2], + }) + case hooks.PostUpdateHook: + hks.PostUpdate(out, err, repoName, args...) + } + + return nil } - ik, err := keygen.New(cfg.Internal.InternalKeyPath, keygen.WithKeyType(keygen.Ed25519)) - if err != nil { - return nil, err + + preReceiveCmd = &cobra.Command{ + Use: "pre-receive", + Short: "Run git pre-receive hook", + RunE: hooksRunE, } - cc := &gossh.ClientConfig{ - User: "internal", - Auth: []gossh.AuthMethod{ - gossh.PublicKeys(ik.Signer()), - }, - HostKeyCallback: gossh.FixedHostKey(pk.PublicKey()), + + updateCmd = &cobra.Command{ + Use: "update", + Short: "Run git update hook", + Args: cobra.ExactArgs(3), + RunE: hooksRunE, } - c, err := gossh.Dial("tcp", cfg.Internal.ListenAddr, cc) - if err != nil { - return nil, err + + postReceiveCmd = &cobra.Command{ + Use: "post-receive", + Short: "Run git post-receive hook", + RunE: hooksRunE, } - return c, nil -} -func newSession(c *gossh.Client) (*gossh.Session, error) { - s, err := c.NewSession() - if err != nil { - return nil, err + postUpdateCmd = &cobra.Command{ + Use: "post-update", + Short: "Run git post-update hook", + RunE: hooksRunE, } - return s, nil +) + +func init() { + hookCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "path to config file") + hookCmd.AddCommand( + preReceiveCmd, + updateCmd, + postReceiveCmd, + postUpdateCmd, + ) } diff --git a/server/backend/backend.go b/server/backend/backend.go index ae243d1a1e6232c09f048b5d8519f9bd3268b801..308f342794b03cc0d416c0530d5529b915b16695 100644 --- a/server/backend/backend.go +++ b/server/backend/backend.go @@ -16,6 +16,7 @@ type Backend interface { RepositoryAccess UserStore UserAccess + Hooks } // ParseAuthorizedKey parses an authorized key string into a public key. diff --git a/server/backend/hooks.go b/server/backend/hooks.go new file mode 100644 index 0000000000000000000000000000000000000000..ba130a30195a734a8b0626524dd6e9c9c9a0c056 --- /dev/null +++ b/server/backend/hooks.go @@ -0,0 +1,20 @@ +package backend + +import ( + "io" +) + +// HookArg is an argument to a git hook. +type HookArg struct { + OldSha string + NewSha string + RefName string +} + +// Hooks provides an interface for git server-side hooks. +type Hooks interface { + PreReceive(stdout io.Writer, stderr io.Writer, repo string, args []HookArg) + Update(stdout io.Writer, stderr io.Writer, repo string, arg HookArg) + PostReceive(stdout io.Writer, stderr io.Writer, repo string, args []HookArg) + PostUpdate(stdout io.Writer, stderr io.Writer, repo string, args ...string) +} diff --git a/server/backend/repo.go b/server/backend/repo.go index 7172d71ee8bf575654c3549ff10f87ec7e5194bc..8d7e9cdeffa516b70c276c383b4d78746ead6876 100644 --- a/server/backend/repo.go +++ b/server/backend/repo.go @@ -29,8 +29,6 @@ type RepositoryStore interface { DeleteRepository(name string) error // RenameRepository renames a repository. RenameRepository(oldName, newName string) error - // InitializeHooks initializes the hooks for the given repository. - InitializeHooks(repo string) error } // RepositoryMetadata is an interface for managing repository metadata. diff --git a/server/backend/sqlite/hooks.go b/server/backend/sqlite/hooks.go new file mode 100644 index 0000000000000000000000000000000000000000..cf82c2c009a3b72415d0ba09b72152521eabe823 --- /dev/null +++ b/server/backend/sqlite/hooks.go @@ -0,0 +1,64 @@ +package sqlite + +import ( + "io" + "sync" + + "github.com/charmbracelet/log" + "github.com/charmbracelet/soft-serve/server/backend" +) + +// PostReceive is called by the git post-receive hook. +// +// It implements Hooks. +func (d *SqliteBackend) PostReceive(stdout io.Writer, stderr io.Writer, repo string, args []backend.HookArg) { + log.WithPrefix("backend.sqlite.hooks").Debug("post-receive hook called", "repo", repo, "args", args) +} + +// PreReceive is called by the git pre-receive hook. +// +// It implements Hooks. +func (d *SqliteBackend) PreReceive(stdout io.Writer, stderr io.Writer, repo string, args []backend.HookArg) { + log.WithPrefix("backend.sqlite.hooks").Debug("pre-receive hook called", "repo", repo, "args", args) +} + +// Update is called by the git update hook. +// +// It implements Hooks. +func (d *SqliteBackend) Update(stdout io.Writer, stderr io.Writer, repo string, arg backend.HookArg) { + log.WithPrefix("backend.sqlite.hooks").Debug("update hook called", "repo", repo, "arg", arg) +} + +// PostUpdate is called by the git post-update hook. +// +// It implements Hooks. +func (d *SqliteBackend) PostUpdate(stdout io.Writer, stderr io.Writer, repo string, args ...string) { + log.WithPrefix("backend.sqlite.hooks").Debug("post-update hook called", "repo", repo, "args", args) + + var wg sync.WaitGroup + + // Update server info + wg.Add(1) + go func() { + defer wg.Done() + + rr, err := d.Repository(repo) + if err != nil { + log.WithPrefix("backend.sqlite.hooks").Error("error getting repository", "repo", repo, "err", err) + return + } + + r, err := rr.Open() + if err != nil { + log.WithPrefix("backend.sqlite.hooks").Error("error opening repository", "repo", repo, "err", err) + return + } + + if err := r.UpdateServerInfo(); err != nil { + log.WithPrefix("backend.sqlite.hooks").Error("error updating server-info", "repo", repo, "err", err) + return + } + }() + + wg.Wait() +} diff --git a/server/backend/sqlite/sqlite.go b/server/backend/sqlite/sqlite.go index 382c99ff3e32005802de520ab5b6dcd273f05303..0720a09710bed98a2a3062482740ae3ddaafa050 100644 --- a/server/backend/sqlite/sqlite.go +++ b/server/backend/sqlite/sqlite.go @@ -1,18 +1,17 @@ package sqlite import ( - "bytes" "context" "fmt" "os" "path/filepath" "strings" - "text/template" "github.com/charmbracelet/log" "github.com/charmbracelet/soft-serve/git" "github.com/charmbracelet/soft-serve/server/backend" "github.com/charmbracelet/soft-serve/server/config" + "github.com/charmbracelet/soft-serve/server/hooks" "github.com/charmbracelet/soft-serve/server/utils" "github.com/jmoiron/sqlx" _ "modernc.org/sqlite" @@ -165,7 +164,7 @@ func (d *SqliteBackend) CreateRepository(name string, opts backend.RepositoryOpt db: d.db, } - return r, d.InitializeHooks(name) + return r, d.initRepo(name) } // ImportRepository imports a repository from remote. @@ -186,7 +185,7 @@ func (d *SqliteBackend) ImportRepository(name string, remote string, opts backen Envs: []string{ fmt.Sprintf(`GIT_SSH_COMMAND=ssh -o UserKnownHostsFile="%s" -o StrictHostKeyChecking=no -i "%s"`, filepath.Join(d.cfg.DataPath, "ssh", "known_hosts"), - d.cfg.Internal.ClientKeyPath, + d.cfg.SSH.ClientKeyPath, ), }, }, @@ -551,157 +550,8 @@ func (d *SqliteBackend) RemoveCollaborator(repo string, username string) error { ) } -var ( - hookNames = []string{"pre-receive", "update", "post-update", "post-receive"} - hookTpls = []string{ - // for pre-receive - `#!/usr/bin/env bash -# AUTO GENERATED BY SOFT SERVE, DO NOT MODIFY -data=$(cat) -exitcodes="" -hookname=$(basename $0) -GIT_DIR=${GIT_DIR:-$(dirname $0)/..} -for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do - test -x "${hook}" && test -f "${hook}" || continue - echo "${data}" | "${hook}" - exitcodes="${exitcodes} $?" -done -for i in ${exitcodes}; do - [ ${i} -eq 0 ] || exit ${i} -done -`, - - // for update - `#!/usr/bin/env bash -# AUTO GENERATED BY SOFT SERVE, DO NOT MODIFY -exitcodes="" -hookname=$(basename $0) -GIT_DIR=${GIT_DIR:-$(dirname $0/..)} -for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do - test -x "${hook}" && test -f "${hook}" || continue - "${hook}" $1 $2 $3 - exitcodes="${exitcodes} $?" -done -for i in ${exitcodes}; do - [ ${i} -eq 0 ] || exit ${i} -done -`, - - // for post-update - `#!/usr/bin/env bash -# AUTO GENERATED BY SOFT SERVE, DO NOT MODIFY -data=$(cat) -exitcodes="" -hookname=$(basename $0) -GIT_DIR=${GIT_DIR:-$(dirname $0)/..} -for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do - test -x "${hook}" && test -f "${hook}" || continue - "${hook}" $@ - exitcodes="${exitcodes} $?" -done -for i in ${exitcodes}; do - [ ${i} -eq 0 ] || exit ${i} -done -`, - - // for post-receive - `#!/usr/bin/env bash -# AUTO GENERATED BY SOFT SERVE, DO NOT MODIFY -data=$(cat) -exitcodes="" -hookname=$(basename $0) -GIT_DIR=${GIT_DIR:-$(dirname $0)/..} -for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do - test -x "${hook}" && test -f "${hook}" || continue - echo "${data}" | "${hook}" - exitcodes="${exitcodes} $?" -done -for i in ${exitcodes}; do - [ ${i} -eq 0 ] || exit ${i} -done -`, - } -) - -// InitializeHooks updates the hooks for the given repository. -// -// It implements backend.Backend. -func (d *SqliteBackend) InitializeHooks(repo string) error { - hookTmpl, err := template.New("hook").Parse(`#!/usr/bin/env bash -# AUTO GENERATED BY SOFT SERVE, DO NOT MODIFY -{{ range $_, $env := .Envs }} -{{ $env }} \{{ end }} -{{ .Executable }} hook --config "{{ .Config }}" {{ .Hook }} {{ .Args }} -`) - if err != nil { - return err - } - - repo = utils.SanitizeRepo(repo) + ".git" - hooksPath := filepath.Join(d.reposPath(), repo, "hooks") - if err := os.MkdirAll(hooksPath, os.ModePerm); err != nil { - return err - } - - ex, err := os.Executable() - if err != nil { - return err - } - - dp, err := filepath.Abs(d.dp) - if err != nil { - return fmt.Errorf("failed to get absolute path for data path: %w", err) - } - - cp := filepath.Join(dp, "config.yaml") - envs := []string{} - for i, hook := range hookNames { - var data bytes.Buffer - var args string - hp := filepath.Join(hooksPath, hook) - if err := os.WriteFile(hp, []byte(hookTpls[i]), os.ModePerm); err != nil { - return err - } - - // Create hook.d directory. - hp += ".d" - if err := os.MkdirAll(hp, os.ModePerm); err != nil { - return err - } - - if hook == "update" { - args = "$1 $2 $3" - } else if hook == "post-update" { - args = "$@" - } - - err = hookTmpl.Execute(&data, struct { - Executable string - Hook string - Args string - Envs []string - Config string - }{ - Executable: ex, - Hook: hook, - Args: args, - Envs: envs, - Config: cp, - }) - if err != nil { - logger.Error("failed to execute hook template", "err", err) - continue - } - - hp = filepath.Join(hp, "soft-serve") - err = os.WriteFile(hp, data.Bytes(), os.ModePerm) //nolint:gosec - if err != nil { - logger.Error("failed to write hook", "err", err) - continue - } - } - - return nil +func (d *SqliteBackend) initRepo(repo string) error { + return hooks.GenerateHooks(d.ctx, d.cfg, repo) } func (d *SqliteBackend) initRepos() error { @@ -711,7 +561,7 @@ func (d *SqliteBackend) initRepos() error { } for _, repo := range repos { - if err := d.InitializeHooks(repo.Name()); err != nil { + if err := d.initRepo(repo.Name()); err != nil { return err } } diff --git a/server/config/config.go b/server/config/config.go index 7c598548430b8f846cea8d1e86bc75892ff44670..7bd696e0e0d18220f3726dea9d781635b6f24fec 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -25,6 +25,9 @@ type SSHConfig struct { // KeyPath is the path to the SSH server's private key. KeyPath string `env:"KEY_PATH" yaml:"key_path"` + // ClientKeyPath is the path to the server's client private key. + ClientKeyPath string `env:"CLIENT_KEY_PATH" yaml:"client_key_path"` + // MaxTimeout is the maximum number of seconds a connection can take. MaxTimeout int `env:"MAX_TIMEOUT" yaml:"max_timeout"` @@ -68,22 +71,6 @@ type StatsConfig struct { ListenAddr string `env:"LISTEN_ADDR" yaml:"listen_addr"` } -// InternalConfig is the configuration for the internal server. -// This is used for internal communication between the Soft Serve client and server. -type InternalConfig struct { - // ListenAddr is the address on which the internal server will listen. - ListenAddr string `env:"LISTEN_ADDR" yaml:"listen_addr"` - - // KeyPath is the path to the SSH server's host private key. - KeyPath string `env:"KEY_PATH" yaml:"key_path"` - - // InternalKeyPath is the path to the server's internal private key. - InternalKeyPath string `env:"INTERNAL_KEY_PATH" yaml:"internal_key_path"` - - // ClientKeyPath is the path to the server's client private key. - ClientKeyPath string `env:"CLIENT_KEY_PATH" yaml:"client_key_path"` -} - // Config is the configuration for Soft Serve. type Config struct { // Name is the name of the server. @@ -101,9 +88,6 @@ type Config struct { // Stats is the configuration for the stats server. Stats StatsConfig `envPrefix:"STATS_" yaml:"stats"` - // Internal is the configuration for the internal server. - Internal InternalConfig `envPrefix:"INTERNAL_" yaml:"internal"` - // InitialAdminKeys is a list of public keys that will be added to the list of admins. InitialAdminKeys []string `env:"INITIAL_ADMIN_KEYS" envSeparator:"\n" yaml:"initial_admin_keys"` @@ -120,11 +104,12 @@ func parseConfig(path string) (*Config, error) { Name: "Soft Serve", DataPath: dataPath, SSH: SSHConfig{ - ListenAddr: ":23231", - PublicURL: "ssh://localhost:23231", - KeyPath: filepath.Join("ssh", "soft_serve_host_ed25519"), - MaxTimeout: 0, - IdleTimeout: 0, + ListenAddr: ":23231", + PublicURL: "ssh://localhost:23231", + KeyPath: filepath.Join("ssh", "soft_serve_host_ed25519"), + ClientKeyPath: filepath.Join("ssh", "soft_serve_client_ed25519"), + MaxTimeout: 0, + IdleTimeout: 0, }, Git: GitConfig{ ListenAddr: ":9418", @@ -139,12 +124,6 @@ func parseConfig(path string) (*Config, error) { Stats: StatsConfig{ ListenAddr: "localhost:23233", }, - Internal: InternalConfig{ - ListenAddr: "localhost:23230", - KeyPath: filepath.Join("ssh", "soft_serve_internal_host_ed25519"), - InternalKeyPath: filepath.Join("ssh", "soft_serve_internal_ed25519"), - ClientKeyPath: filepath.Join("ssh", "soft_serve_client_ed25519"), - }, } f, err := os.Open(path) @@ -260,16 +239,8 @@ func (c *Config) validate() error { c.SSH.KeyPath = filepath.Join(c.DataPath, c.SSH.KeyPath) } - if c.Internal.KeyPath != "" && !filepath.IsAbs(c.Internal.KeyPath) { - c.Internal.KeyPath = filepath.Join(c.DataPath, c.Internal.KeyPath) - } - - if c.Internal.ClientKeyPath != "" && !filepath.IsAbs(c.Internal.ClientKeyPath) { - c.Internal.ClientKeyPath = filepath.Join(c.DataPath, c.Internal.ClientKeyPath) - } - - if c.Internal.InternalKeyPath != "" && !filepath.IsAbs(c.Internal.InternalKeyPath) { - c.Internal.InternalKeyPath = filepath.Join(c.DataPath, c.Internal.InternalKeyPath) + if c.SSH.ClientKeyPath != "" && !filepath.IsAbs(c.SSH.ClientKeyPath) { + c.SSH.ClientKeyPath = filepath.Join(c.DataPath, c.SSH.ClientKeyPath) } if c.HTTP.TLSKeyPath != "" && !filepath.IsAbs(c.HTTP.TLSKeyPath) { @@ -298,7 +269,7 @@ func parseAuthKeys(aks []string) []ssh.PublicKey { return pks } -// AdminKeys returns the admin keys including the internal api key. +// AdminKeys returns the server admin keys. func (c *Config) AdminKeys() []ssh.PublicKey { - return parseAuthKeys(append(c.InitialAdminKeys, c.Internal.InternalKeyPath)) + return parseAuthKeys(c.InitialAdminKeys) } diff --git a/server/config/file.go b/server/config/file.go index eb462ae7061b7477ab999802b5cb9efa212eab11..1869dd87fcfd72aca7c71fb50cf6a19e49231f9e 100644 --- a/server/config/file.go +++ b/server/config/file.go @@ -24,6 +24,10 @@ ssh: # The path to the SSH server's private key. key_path: "{{ .SSH.KeyPath }}" + # The path to the server's client private key. This key will be used to + # authenticate the server to make git requests to ssh remotes. + client_key_path: "{{ .Internal.ClientKeyPath }}" + # The maximum number of seconds a connection can take. # A value of 0 means no timeout. max_timeout: {{ .SSH.MaxTimeout }} @@ -68,22 +72,6 @@ stats: # The address on which the stats server will listen. listen_addr: "{{ .Stats.ListenAddr }}" -# The internal server configuration. -internal: - # The address on which the internal server will listen. - listen_addr: "{{ .Internal.ListenAddr }}" - - # The path to the Internal server's host private key. - key_path: "{{ .Internal.KeyPath }}" - - # The path to the Internal server's client private key. - # This key will be used to authenticate the server to make git requests to - # ssh remotes. - client_key_path: "{{ .Internal.ClientKeyPath }}" - - # The path to the Internal server's internal api private key. - internal_key_path: "{{ .Internal.InternalKeyPath }}" - # Additional admin keys. #initial_admin_keys: # - "ssh-rsa AAAAB3NzaC1yc2..." diff --git a/server/daemon/daemon.go b/server/daemon/daemon.go index 1a10c8c3bc204183e9362f74a0da948e8e816621..c3e08381502b6571fb4d3595627790680d053965 100644 --- a/server/daemon/daemon.go +++ b/server/daemon/daemon.go @@ -253,7 +253,13 @@ func (d *GitDaemon) handleClient(conn net.Conn) { return } - if err := gitPack(c, c, c, filepath.Join(reposDir, repo)); err != nil { + // Environment variables to pass down to git hooks. + envs := []string{ + "SOFT_SERVE_REPO_NAME=" + name, + "SOFT_SERVE_REPO_PATH=" + filepath.Join(reposDir, repo), + } + + if err := gitPack(ctx, c, c, c, filepath.Join(reposDir, repo), envs...); err != nil { fatal(c, err) return } diff --git a/server/git/git.go b/server/git/git.go index 53c192d5468cbecf84ea8d740945e9cc8c0d5ce0..9a8b5ed9d401616151e7a47ced47a5ad9ff40dbc 100644 --- a/server/git/git.go +++ b/server/git/git.go @@ -1,16 +1,19 @@ package git import ( + "context" "errors" "fmt" "io" "os" + "os/exec" "path/filepath" "strings" "github.com/charmbracelet/log" "github.com/charmbracelet/soft-serve/git" "github.com/go-git/go-git/v5/plumbing/format/pktline" + "golang.org/x/sync/errgroup" ) var ( @@ -42,7 +45,7 @@ const ( ) // UploadPack runs the git upload-pack protocol against the provided repo. -func UploadPack(in io.Reader, out io.Writer, er io.Writer, repoDir string) error { +func UploadPack(ctx context.Context, in io.Reader, out io.Writer, er io.Writer, repoDir string, envs ...string) error { exists, err := fileExists(repoDir) if !exists { return ErrInvalidRepo @@ -50,11 +53,11 @@ func UploadPack(in io.Reader, out io.Writer, er io.Writer, repoDir string) error if err != nil { return err } - return RunGit(in, out, er, "", UploadPackBin[4:], repoDir) + return RunGit(ctx, in, out, er, "", envs, UploadPackBin[4:], repoDir) } // UploadArchive runs the git upload-archive protocol against the provided repo. -func UploadArchive(in io.Reader, out io.Writer, er io.Writer, repoDir string) error { +func UploadArchive(ctx context.Context, in io.Reader, out io.Writer, er io.Writer, repoDir string, envs ...string) error { exists, err := fileExists(repoDir) if !exists { return ErrInvalidRepo @@ -62,25 +65,77 @@ func UploadArchive(in io.Reader, out io.Writer, er io.Writer, repoDir string) er if err != nil { return err } - return RunGit(in, out, er, "", UploadArchiveBin[4:], repoDir) + return RunGit(ctx, in, out, er, "", envs, UploadArchiveBin[4:], repoDir) } // ReceivePack runs the git receive-pack protocol against the provided repo. -func ReceivePack(in io.Reader, out io.Writer, er io.Writer, repoDir string) error { - if err := RunGit(in, out, er, "", ReceivePackBin[4:], repoDir); err != nil { +func ReceivePack(ctx context.Context, in io.Reader, out io.Writer, er io.Writer, repoDir string, envs ...string) error { + if err := RunGit(ctx, in, out, er, "", envs, ReceivePackBin[4:], repoDir); err != nil { return err } - return EnsureDefaultBranch(in, out, er, repoDir) + return EnsureDefaultBranch(ctx, in, out, er, repoDir) } // RunGit runs a git command in the given repo. -func RunGit(in io.Reader, out io.Writer, err io.Writer, dir string, args ...string) error { - c := git.NewCommand(args...) - return c.RunInDirWithOptions(dir, git.RunInDirOptions{ - Stdin: in, - Stdout: out, - Stderr: err, +func RunGit(ctx context.Context, in io.Reader, out io.Writer, er io.Writer, dir string, envs []string, args ...string) error { + logger := log.WithPrefix("server.git") + c := exec.CommandContext(ctx, "git", args...) + c.Dir = dir + c.Env = append(c.Env, envs...) + c.Env = append(c.Env, "SOFT_SERVE_DEBUG="+os.Getenv("SOFT_SERVE_DEBUG")) + c.Env = append(c.Env, "PATH="+os.Getenv("PATH")) + + stdin, err := c.StdinPipe() + if err != nil { + logger.Error("failed to get stdin pipe", "err", err) + return err + } + + stdout, err := c.StdoutPipe() + if err != nil { + logger.Error("failed to get stdout pipe", "err", err) + return err + } + + stderr, err := c.StderrPipe() + if err != nil { + logger.Error("failed to get stderr pipe", "err", err) + return err + } + + if err := c.Start(); err != nil { + logger.Error("failed to start command", "err", err) + return err + } + + errg, ctx := errgroup.WithContext(ctx) + + // stdin + errg.Go(func() error { + defer stdin.Close() + + _, err := io.Copy(stdin, in) + return err + }) + + // stdout + errg.Go(func() error { + _, err := io.Copy(out, stdout) + return err + }) + + // stderr + errg.Go(func() error { + _, err := io.Copy(er, stderr) + return err }) + + if err := errg.Wait(); err != nil { + logger.Error("while running git command", "err", err) + return err + } + + return nil } // WritePktline encodes and writes a pktline to the given writer. @@ -129,7 +184,7 @@ func fileExists(path string) (bool, error) { return true, err } -func EnsureDefaultBranch(in io.Reader, out io.Writer, er io.Writer, repoPath string) error { +func EnsureDefaultBranch(ctx context.Context, in io.Reader, out io.Writer, er io.Writer, repoPath string) error { r, err := git.Open(repoPath) if err != nil { return err @@ -144,7 +199,7 @@ func EnsureDefaultBranch(in io.Reader, out io.Writer, er io.Writer, repoPath str // Rename the default branch to the first branch available _, err = r.HEAD() if err == git.ErrReferenceNotExist { - err = RunGit(in, out, er, repoPath, "branch", "-M", brs[0]) + err = RunGit(ctx, in, out, er, repoPath, []string{}, "branch", "-M", brs[0]) if err != nil { return err } diff --git a/server/hooks.go b/server/hooks.go deleted file mode 100644 index 17cc4c83e108cd1f24e4e1f72c1b413d95dd294b..0000000000000000000000000000000000000000 --- a/server/hooks.go +++ /dev/null @@ -1,54 +0,0 @@ -package server - -import ( - "io" - - "github.com/charmbracelet/log" - "github.com/charmbracelet/soft-serve/server/hooks" -) - -var _ hooks.Hooks = (*Server)(nil) - -// PostReceive is called by the git post-receive hook. -// -// It implements Hooks. -func (*Server) PostReceive(stdin io.Reader, stdout io.Writer, stderr io.Writer, repo string, args []hooks.HookArg) { - log.WithPrefix("server.hooks").Debug("post-receive hook called", "repo", repo, "args", args) -} - -// PreReceive is called by the git pre-receive hook. -// -// It implements Hooks. -func (*Server) PreReceive(stdin io.Reader, stdout io.Writer, stderr io.Writer, repo string, args []hooks.HookArg) { - log.WithPrefix("server.hooks").Debug("pre-receive hook called", "repo", repo, "args", args) -} - -// Update is called by the git update hook. -// -// It implements Hooks. -func (*Server) Update(stdin io.Reader, stdout io.Writer, stderr io.Writer, repo string, arg hooks.HookArg) { - log.WithPrefix("server.hooks").Debug("update hook called", "repo", repo, "arg", arg) -} - -// PostUpdate is called by the git post-update hook. -// -// It implements Hooks. -func (s *Server) PostUpdate(stdin io.Reader, stdout io.Writer, stderr io.Writer, repo string, args ...string) { - log.WithPrefix("server.hooks").Debug("post-update hook called", "repo", repo, "args", args) - rr, err := s.Config.Backend.Repository(repo) - if err != nil { - log.WithPrefix("server.hooks.post-update").Error("error getting repository", "repo", repo, "err", err) - return - } - - r, err := rr.Open() - if err != nil { - log.WithPrefix("server.hooks.post-update").Error("error opening repository", "repo", repo, "err", err) - return - } - - if err := r.UpdateServerInfo(); err != nil { - log.WithPrefix("server.hooks.post-update").Error("error updating server info", "repo", repo, "err", err) - return - } -} diff --git a/server/hooks/hooks.go b/server/hooks/hooks.go index 639950413741d4cebd47eebfcffc47b0e5107303..f7606bdd599d66ffcc0bb4cac24684fb1b1a3ca8 100644 --- a/server/hooks/hooks.go +++ b/server/hooks/hooks.go @@ -1,18 +1,152 @@ package hooks -import "io" +import ( + "bytes" + "context" + "fmt" + "os" + "path/filepath" + "text/template" -// HookArg is an argument to a git hook. -type HookArg struct { - OldSha string - NewSha string - RefName string -} + "github.com/charmbracelet/log" + "github.com/charmbracelet/soft-serve/server/config" + "github.com/charmbracelet/soft-serve/server/utils" +) + +// The names of git server-side hooks. +const ( + PreReceiveHook = "pre-receive" + UpdateHook = "update" + PostReceiveHook = "post-receive" + PostUpdateHook = "post-update" +) + +// GenerateHooks generates git server-side hooks for a repository. Currently, it supports the following hooks: +// - pre-receive +// - update +// - post-receive +// - post-update +// +// This function should be called by the backend when a repository is created. +// TODO: support context +func GenerateHooks(ctx context.Context, cfg *config.Config, repo string) error { + repo = utils.SanitizeRepo(repo) + ".git" + hooksPath := filepath.Join(cfg.DataPath, "repos", repo, "hooks") + if err := os.MkdirAll(hooksPath, os.ModePerm); err != nil { + return err + } + + ex, err := os.Executable() + if err != nil { + return err + } + + dp, err := filepath.Abs(cfg.DataPath) + if err != nil { + return fmt.Errorf("failed to get absolute path for data path: %w", err) + } + + cp := filepath.Join(dp, "config.yaml") + // Add extra environment variables to the hooks here. + envs := []string{} + + for _, hook := range []string{ + PreReceiveHook, + UpdateHook, + PostReceiveHook, + PostUpdateHook, + } { + var data bytes.Buffer + var args string + + // Hooks script/directory path + hp := filepath.Join(hooksPath, hook) + + // Write the hooks primary script + if err := os.WriteFile(hp, []byte(hookTemplate), os.ModePerm); err != nil { + return err + } -// Hooks provides an interface for git server-side hooks. -type Hooks interface { - PreReceive(stdin io.Reader, stdout io.Writer, stderr io.Writer, repo string, args []HookArg) - Update(stdin io.Reader, stdout io.Writer, stderr io.Writer, repo string, arg HookArg) - PostReceive(stdin io.Reader, stdout io.Writer, stderr io.Writer, repo string, args []HookArg) - PostUpdate(stdin io.Reader, stdout io.Writer, stderr io.Writer, repo string, args ...string) + // Create ${hook}.d directory. + hp += ".d" + if err := os.MkdirAll(hp, os.ModePerm); err != nil { + return err + } + + switch hook { + case UpdateHook: + args = "$1 $2 $3" + case PostUpdateHook: + args = "$@" + } + + if err := hooksTmpl.Execute(&data, struct { + Executable string + Config string + Envs []string + Hook string + Args string + }{ + Executable: ex, + Config: cp, + Envs: envs, + Hook: hook, + Args: args, + }); err != nil { + log.WithPrefix("backend.hooks").Error("failed to execute hook template", "err", err) + continue + } + + // Write the soft-serve hook inside ${hook}.d directory. + hp = filepath.Join(hp, "soft-serve") + err = os.WriteFile(hp, data.Bytes(), os.ModePerm) //nolint:gosec + if err != nil { + log.WithPrefix("backend.hooks").Error("failed to write hook", "err", err) + continue + } + } + + return nil } + +const ( + // hookTemplate allows us to run multiple hooks from a directory. It should + // support every type of git hook, as it proxies both stdin and arguments. + hookTemplate = `#!/usr/bin/env bash +# AUTO GENERATED BY SOFT SERVE, DO NOT MODIFY +data=$(cat) +exitcodes="" +hookname=$(basename $0) +GIT_DIR=${GIT_DIR:-$(dirname $0)/..} +for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do + # Avoid running non-executable hooks + test -x "${hook}" && test -f "${hook}" || continue + + # Run the actual hook + echo "${data}" | "${hook}" "$@" + + # Store the exit code for later use + exitcodes="${exitcodes} $?" +done + +# Exit on the first non-zero exit code. +for i in ${exitcodes}; do + [ ${i} -eq 0 ] || exit ${i} +done +` +) + +var ( + // hooksTmpl is the soft-serve hook that will be run by the git hooks + // inside the hooks directory. + hooksTmpl = template.Must(template.New("hooks").Parse(`#!/usr/bin/env bash +# AUTO GENERATED BY SOFT SERVE, DO NOT MODIFY +if [ -z "$SOFT_SERVE_REPO_NAME" ]; then + echo "Warning: SOFT_SERVE_REPO_NAME not defined. Skipping hooks." + exit 0 +fi +{{ range $_, $env := .Envs }} +{{ $env }} \{{ end }} +{{ .Executable }} hook --config "{{ .Config }}" {{ .Hook }} {{ .Args }} +`)) +) diff --git a/server/internal/cmd.go b/server/internal/cmd.go deleted file mode 100644 index 62709bed11248fa73e540decbb53ca03a437921e..0000000000000000000000000000000000000000 --- a/server/internal/cmd.go +++ /dev/null @@ -1,84 +0,0 @@ -package internal - -import ( - "context" - - "github.com/charmbracelet/soft-serve/server/config" - "github.com/charmbracelet/soft-serve/server/hooks" - "github.com/charmbracelet/ssh" - "github.com/charmbracelet/wish" - "github.com/spf13/cobra" -) - -var ( - hooksCtxKey = "hooks" - sessionCtxKey = "session" - configCtxKey = "config" -) - -// rootCommand is the root command for the server. -func rootCommand(cfg *config.Config, s ssh.Session) *cobra.Command { - rootCmd := &cobra.Command{ - Short: "Soft Serve internal API.", - SilenceUsage: true, - } - - rootCmd.SetIn(s) - rootCmd.SetOut(s) - rootCmd.SetErr(s) - rootCmd.CompletionOptions.DisableDefaultCmd = true - - rootCmd.AddCommand( - hookCommand(), - ) - - return rootCmd -} - -// Middleware returns the middleware for the server. -func (i *InternalServer) Middleware(hooks hooks.Hooks) wish.Middleware { - return func(sh ssh.Handler) ssh.Handler { - return func(s ssh.Session) { - _, _, active := s.Pty() - if active { - return - } - - // Ignore git server commands. - args := s.Command() - if len(args) > 0 { - if args[0] == "git-receive-pack" || - args[0] == "git-upload-pack" || - args[0] == "git-upload-archive" { - return - } - } - - ctx := context.WithValue(s.Context(), hooksCtxKey, hooks) - ctx = context.WithValue(ctx, sessionCtxKey, s) - ctx = context.WithValue(ctx, configCtxKey, i.cfg) - - rootCmd := rootCommand(i.cfg, s) - rootCmd.SetArgs(args) - if len(args) == 0 { - // otherwise it'll default to os.Args, which is not what we want. - rootCmd.SetArgs([]string{"--help"}) - } - rootCmd.SetIn(s) - rootCmd.SetOut(s) - rootCmd.CompletionOptions.DisableDefaultCmd = true - rootCmd.SetErr(s.Stderr()) - if err := rootCmd.ExecuteContext(ctx); err != nil { - _ = s.Exit(1) - } - sh(s) - } - } -} - -func fromContext(cmd *cobra.Command) (*config.Config, ssh.Session) { - ctx := cmd.Context() - cfg := ctx.Value(configCtxKey).(*config.Config) - s := ctx.Value(sessionCtxKey).(ssh.Session) - return cfg, s -} diff --git a/server/internal/hook.go b/server/internal/hook.go deleted file mode 100644 index a4d4ecbaf7254f85ea798f0026009b8154724ba7..0000000000000000000000000000000000000000 --- a/server/internal/hook.go +++ /dev/null @@ -1,138 +0,0 @@ -package internal - -import ( - "bufio" - "fmt" - "strings" - - "github.com/charmbracelet/keygen" - "github.com/charmbracelet/log" - "github.com/charmbracelet/soft-serve/server/backend" - "github.com/charmbracelet/soft-serve/server/errors" - "github.com/charmbracelet/soft-serve/server/hooks" - "github.com/charmbracelet/ssh" - "github.com/spf13/cobra" -) - -// hookCommand handles Soft Serve internal API git hook requests. -func hookCommand() *cobra.Command { - preReceiveCmd := &cobra.Command{ - Use: "pre-receive", - Short: "Run git pre-receive hook", - RunE: func(cmd *cobra.Command, args []string) error { - _, s := fromContext(cmd) - hks := cmd.Context().Value(hooksCtxKey).(hooks.Hooks) - repoName := getRepoName(s) - opts := make([]hooks.HookArg, 0) - scanner := bufio.NewScanner(s) - for scanner.Scan() { - fields := strings.Fields(scanner.Text()) - if len(fields) != 3 { - return fmt.Errorf("invalid pre-receive hook input: %s", scanner.Text()) - } - opts = append(opts, hooks.HookArg{ - OldSha: fields[0], - NewSha: fields[1], - RefName: fields[2], - }) - } - hks.PreReceive(s, s, s.Stderr(), repoName, opts) - return nil - }, - } - - updateCmd := &cobra.Command{ - Use: "update", - Short: "Run git update hook", - Args: cobra.ExactArgs(3), - RunE: func(cmd *cobra.Command, args []string) error { - _, s := fromContext(cmd) - hks := cmd.Context().Value(hooksCtxKey).(hooks.Hooks) - repoName := getRepoName(s) - hks.Update(s, s, s.Stderr(), repoName, hooks.HookArg{ - RefName: args[0], - OldSha: args[1], - NewSha: args[2], - }) - return nil - }, - } - - postReceiveCmd := &cobra.Command{ - Use: "post-receive", - Short: "Run git post-receive hook", - RunE: func(cmd *cobra.Command, _ []string) error { - _, s := fromContext(cmd) - hks := cmd.Context().Value(hooksCtxKey).(hooks.Hooks) - repoName := getRepoName(s) - opts := make([]hooks.HookArg, 0) - scanner := bufio.NewScanner(s) - for scanner.Scan() { - fields := strings.Fields(scanner.Text()) - if len(fields) != 3 { - return fmt.Errorf("invalid post-receive hook input: %s", scanner.Text()) - } - opts = append(opts, hooks.HookArg{ - OldSha: fields[0], - NewSha: fields[1], - RefName: fields[2], - }) - } - hks.PostReceive(s, s, s.Stderr(), repoName, opts) - return nil - }, - } - - postUpdateCmd := &cobra.Command{ - Use: "post-update", - Short: "Run git post-update hook", - RunE: func(cmd *cobra.Command, args []string) error { - _, s := fromContext(cmd) - hks := cmd.Context().Value(hooksCtxKey).(hooks.Hooks) - repoName := getRepoName(s) - hks.PostUpdate(s, s, s.Stderr(), repoName, args...) - return nil - }, - } - - hookCmd := &cobra.Command{ - Use: "hook", - Short: "Run git server hooks", - Hidden: true, - SilenceUsage: true, - } - - hookCmd.AddCommand( - preReceiveCmd, - updateCmd, - postReceiveCmd, - postUpdateCmd, - ) - - return hookCmd -} - -// Check if the session's public key matches the internal API key. -func checkIfInternal(cmd *cobra.Command, _ []string) error { - cfg, s := fromContext(cmd) - pk := s.PublicKey() - kp, err := keygen.New(cfg.Internal.InternalKeyPath, keygen.WithKeyType(keygen.Ed25519)) - if err != nil { - log.WithPrefix("server.internal").Errorf("failed to read internal key: %v", err) - return err - } - if !backend.KeysEqual(pk, kp.PublicKey()) { - return errors.ErrUnauthorized - } - return nil -} - -func getRepoName(s ssh.Session) string { - var repoName string - for _, env := range s.Environ() { - if strings.HasPrefix(env, "SOFT_SERVE_REPO_NAME=") { - return strings.TrimPrefix(env, "SOFT_SERVE_REPO_NAME=") - } - } - return repoName -} diff --git a/server/internal/internal.go b/server/internal/internal.go deleted file mode 100644 index 52114c4ca0afbfdc2fd1da7fa81d914c68139777..0000000000000000000000000000000000000000 --- a/server/internal/internal.go +++ /dev/null @@ -1,86 +0,0 @@ -package internal - -import ( - "context" - "fmt" - - "github.com/charmbracelet/keygen" - "github.com/charmbracelet/soft-serve/server/backend" - "github.com/charmbracelet/soft-serve/server/config" - "github.com/charmbracelet/soft-serve/server/hooks" - "github.com/charmbracelet/ssh" - "github.com/charmbracelet/wish" -) - -// InternalServer is a internal interface to communicate with the server. -type InternalServer struct { - cfg *config.Config - s *ssh.Server - kp *keygen.SSHKeyPair - ckp *keygen.SSHKeyPair -} - -// NewInternalServer returns a new internal server. -func NewInternalServer(cfg *config.Config, hooks hooks.Hooks) (*InternalServer, error) { - i := &InternalServer{cfg: cfg} - - // Create internal key. - ikp, err := keygen.New( - cfg.Internal.InternalKeyPath, - keygen.WithKeyType(keygen.Ed25519), - keygen.WithWrite(), - ) - if err != nil { - return nil, fmt.Errorf("internal key: %w", err) - } - - i.kp = ikp - - // Create client key. - ckp, err := keygen.New( - cfg.Internal.ClientKeyPath, - keygen.WithKeyType(keygen.Ed25519), - keygen.WithWrite(), - ) - if err != nil { - return nil, fmt.Errorf("client key: %w", err) - } - - i.ckp = ckp - - s, err := wish.NewServer( - wish.WithAddress(cfg.Internal.ListenAddr), - wish.WithHostKeyPath(cfg.Internal.KeyPath), - wish.WithPublicKeyAuth(i.PublicKeyHandler), - wish.WithMiddleware( - i.Middleware(hooks), - ), - ) - if err != nil { - return nil, fmt.Errorf("wish: %w", err) - } - - i.s = s - - return i, nil -} - -// PublicKeyHandler handles public key authentication. -func (i *InternalServer) PublicKeyHandler(ctx ssh.Context, pk ssh.PublicKey) bool { - return backend.KeysEqual(i.kp.PublicKey(), pk) -} - -// Start starts the internal server. -func (i *InternalServer) Start() error { - return i.s.ListenAndServe() -} - -// Shutdown shuts down the internal server. -func (i *InternalServer) Shutdown(ctx context.Context) error { - return i.s.Shutdown(ctx) -} - -// Close closes the internal server. -func (i *InternalServer) Close() error { - return i.s.Close() -} diff --git a/server/jobs.go b/server/jobs.go index 37ecbeb36a33c540a38bfd5ace33615f0cce05ce..5f239be583961649353568d5048042e497f8bfb6 100644 --- a/server/jobs.go +++ b/server/jobs.go @@ -38,7 +38,7 @@ func mirrorJob(cfg *config.Config) func() { cmd.AddEnvs( fmt.Sprintf(`GIT_SSH_COMMAND=ssh -o UserKnownHostsFile="%s" -o StrictHostKeyChecking=no -i "%s"`, filepath.Join(cfg.DataPath, "ssh", "known_hosts"), - cfg.Internal.ClientKeyPath, + cfg.SSH.ClientKeyPath, ), ) if _, err := cmd.RunInDir(r.Path); err != nil { diff --git a/server/server.go b/server/server.go index ecc2c7c357897d71adc4610fb252ca357a291470..182b20f6662cb49c3a8655f84b5b06d806f047cd 100644 --- a/server/server.go +++ b/server/server.go @@ -13,7 +13,6 @@ import ( "github.com/charmbracelet/soft-serve/server/config" "github.com/charmbracelet/soft-serve/server/cron" "github.com/charmbracelet/soft-serve/server/daemon" - "github.com/charmbracelet/soft-serve/server/internal" sshsrv "github.com/charmbracelet/soft-serve/server/ssh" "github.com/charmbracelet/soft-serve/server/stats" "github.com/charmbracelet/soft-serve/server/web" @@ -27,15 +26,14 @@ var ( // Server is the Soft Serve server. type Server struct { - SSHServer *sshsrv.SSHServer - GitDaemon *daemon.GitDaemon - HTTPServer *web.HTTPServer - StatsServer *stats.StatsServer - InternalServer *internal.InternalServer - Cron *cron.CronScheduler - Config *config.Config - Backend backend.Backend - ctx context.Context + SSHServer *sshsrv.SSHServer + GitDaemon *daemon.GitDaemon + HTTPServer *web.HTTPServer + StatsServer *stats.StatsServer + Cron *cron.CronScheduler + Config *config.Config + Backend backend.Backend + ctx context.Context } // NewServer returns a new *ssh.Server configured to serve Soft Serve. The SSH @@ -84,11 +82,6 @@ func NewServer(ctx context.Context, cfg *config.Config) (*Server, error) { return nil, fmt.Errorf("create stats server: %w", err) } - srv.InternalServer, err = internal.NewInternalServer(cfg, srv) - if err != nil { - return nil, fmt.Errorf("create internal server: %w", err) - } - return srv, nil } @@ -143,13 +136,6 @@ func (s *Server) Start() error { s.Cron.Start() return nil }) - errg.Go(func() error { - logger.Print("Starting internal server", "addr", s.Config.Internal.ListenAddr) - if err := start(ctx, s.InternalServer.Start); !errors.Is(err, http.ErrServerClosed) { - return err - } - return nil - }) return errg.Wait() } @@ -172,9 +158,6 @@ func (s *Server) Shutdown(ctx context.Context) error { s.Cron.Stop() return nil }) - errg.Go(func() error { - return s.InternalServer.Shutdown(ctx) - }) return errg.Wait() } @@ -189,6 +172,5 @@ func (s *Server) Close() error { s.Cron.Stop() return nil }) - errg.Go(s.InternalServer.Close) return errg.Wait() } diff --git a/server/ssh/ssh.go b/server/ssh/ssh.go index a407d8fc929541f26ff18baba0e511163a247775..419ab241d2e335f8b6c4ca8ae6330177c77dce4f 100644 --- a/server/ssh/ssh.go +++ b/server/ssh/ssh.go @@ -189,6 +189,13 @@ func (s *SSHServer) Middleware(cfg *config.Config) wish.Middleware { return } + // Environment variables to pass down to git hooks. + envs := []string{ + "SOFT_SERVE_REPO_NAME=" + name, + "SOFT_SERVE_REPO_PATH=" + filepath.Join(reposDir, repo), + "SOFT_SERVE_PUBLIC_KEY=" + ak, + } + logger.Debug("git middleware", "cmd", gc, "access", access.String()) repoDir := filepath.Join(reposDir, repo) switch gc { @@ -205,7 +212,7 @@ func (s *SSHServer) Middleware(cfg *config.Config) wish.Middleware { } createRepoCounter.WithLabelValues(ak, s.User(), name).Inc() } - if err := git.ReceivePack(s, s, s.Stderr(), repoDir); err != nil { + if err := git.ReceivePack(s.Context(), s, s, s.Stderr(), repoDir, envs...); err != nil { sshFatal(s, git.ErrSystemMalfunction) } receivePackCounter.WithLabelValues(ak, s.User(), name).Inc() @@ -223,7 +230,7 @@ func (s *SSHServer) Middleware(cfg *config.Config) wish.Middleware { counter = uploadArchiveCounter } - err := gitPack(s, s, s.Stderr(), repoDir) + err := gitPack(s.Context(), s, s, s.Stderr(), repoDir, envs...) if errors.Is(err, git.ErrInvalidRepo) { sshFatal(s, git.ErrInvalidRepo) } else if err != nil { diff --git a/server/test/test.go b/server/test/test.go index 2dfe85c97dea227186565a04ef3ad86b31e120f8..a9d4d0eca7ac48899385dd23254c66f1249e2855 100644 --- a/server/test/test.go +++ b/server/test/test.go @@ -2,6 +2,8 @@ package test import "net" +// RandomPort returns a random port number. +// This is mainly used for testing. func RandomPort() int { addr, _ := net.Listen("tcp", ":0") //nolint:gosec _ = addr.Close()