git.go

  1package ssh
  2
  3import (
  4	"errors"
  5	"path/filepath"
  6	"time"
  7
  8	"github.com/charmbracelet/log"
  9	"github.com/charmbracelet/soft-serve/server/access"
 10	"github.com/charmbracelet/soft-serve/server/backend"
 11	"github.com/charmbracelet/soft-serve/server/config"
 12	"github.com/charmbracelet/soft-serve/server/git"
 13	"github.com/charmbracelet/soft-serve/server/proto"
 14	"github.com/charmbracelet/soft-serve/server/sshutils"
 15	"github.com/charmbracelet/soft-serve/server/utils"
 16	"github.com/charmbracelet/ssh"
 17)
 18
 19func handleGit(s ssh.Session) {
 20	ctx := s.Context()
 21	cfg := config.FromContext(ctx)
 22	be := backend.FromContext(ctx)
 23	logger := log.FromContext(ctx)
 24	cmdLine := s.Command()
 25	start := time.Now()
 26
 27	user, ok := ctx.Value(proto.ContextKeyUser).(proto.User)
 28	if !ok {
 29		sshFatal(s, errors.New("no user in context"))
 30		return
 31	}
 32
 33	// repo should be in the form of "repo.git"
 34	name := utils.SanitizeRepo(cmdLine[1])
 35	pk := s.PublicKey()
 36	ak := sshutils.MarshalAuthorizedKey(pk)
 37	accessLevel := be.AccessLevelForUser(ctx, name, user)
 38	// git bare repositories should end in ".git"
 39	// https://git-scm.com/docs/gitrepository-layout
 40	repo := name + ".git"
 41	reposDir := filepath.Join(cfg.DataPath, "repos")
 42	if err := git.EnsureWithin(reposDir, repo); err != nil {
 43		sshFatal(s, err)
 44		return
 45	}
 46
 47	// Environment variables to pass down to git hooks.
 48	envs := []string{
 49		"SOFT_SERVE_REPO_NAME=" + name,
 50		"SOFT_SERVE_REPO_PATH=" + filepath.Join(reposDir, repo),
 51		"SOFT_SERVE_PUBLIC_KEY=" + ak,
 52		"SOFT_SERVE_USERNAME=" + user.Username(),
 53		"SOFT_SERVE_LOG_PATH=" + filepath.Join(cfg.DataPath, "log", "hooks.log"),
 54	}
 55
 56	// Add ssh session & config environ
 57	envs = append(envs, s.Environ()...)
 58	envs = append(envs, cfg.Environ()...)
 59
 60	repoDir := filepath.Join(reposDir, repo)
 61	service := git.Service(cmdLine[0])
 62	cmd := git.ServiceCommand{
 63		Stdin:  s,
 64		Stdout: s,
 65		Stderr: s.Stderr(),
 66		Env:    envs,
 67		Dir:    repoDir,
 68	}
 69
 70	logger.Debug("git middleware", "cmd", service, "access", accessLevel.String())
 71
 72	switch service {
 73	case git.ReceivePackService:
 74		receivePackCounter.WithLabelValues(name).Inc()
 75		defer func() {
 76			receivePackSeconds.WithLabelValues(name).Add(time.Since(start).Seconds())
 77		}()
 78		if accessLevel < access.ReadWriteAccess {
 79			sshFatal(s, git.ErrNotAuthed)
 80			return
 81		}
 82		if _, err := be.Repository(ctx, name); err != nil {
 83			if _, err := be.CreateRepository(ctx, name, proto.RepositoryOptions{Private: false}); err != nil {
 84				log.Errorf("failed to create repo: %s", err)
 85				sshFatal(s, err)
 86				return
 87			}
 88			createRepoCounter.WithLabelValues(name).Inc()
 89		}
 90
 91		if err := git.ReceivePack(ctx, cmd); err != nil {
 92			sshFatal(s, git.ErrSystemMalfunction)
 93		}
 94
 95		if err := git.EnsureDefaultBranch(ctx, cmd); err != nil {
 96			sshFatal(s, git.ErrSystemMalfunction)
 97		}
 98
 99		receivePackCounter.WithLabelValues(name).Inc()
100		return
101	case git.UploadPackService, git.UploadArchiveService:
102		if accessLevel < access.ReadOnlyAccess {
103			sshFatal(s, git.ErrNotAuthed)
104			return
105		}
106
107		handler := git.UploadPack
108		switch service {
109		case git.UploadArchiveService:
110			handler = git.UploadArchive
111			uploadArchiveCounter.WithLabelValues(name).Inc()
112			defer func() {
113				uploadArchiveSeconds.WithLabelValues(name).Add(time.Since(start).Seconds())
114			}()
115		default:
116			uploadPackCounter.WithLabelValues(name).Inc()
117			defer func() {
118				uploadPackSeconds.WithLabelValues(name).Add(time.Since(start).Seconds())
119			}()
120		}
121
122		err := handler(ctx, cmd)
123		if errors.Is(err, git.ErrInvalidRepo) {
124			sshFatal(s, git.ErrInvalidRepo)
125		} else if err != nil {
126			logger.Error("git middleware", "err", err)
127			sshFatal(s, git.ErrSystemMalfunction)
128		}
129	case git.LFSTransferService:
130		if accessLevel < access.ReadWriteAccess {
131			sshFatal(s, git.ErrNotAuthed)
132			return
133		}
134
135		if len(cmdLine) != 3 ||
136			(cmdLine[2] != "download" && cmdLine[2] != "upload") {
137			sshFatal(s, git.ErrInvalidRequest)
138			return
139		}
140
141		cmd.Args = []string{
142			name,
143			cmdLine[2],
144		}
145
146		if err := git.LFSTransfer(ctx, cmd); err != nil {
147			logger.Error("git middleware", "err", err)
148			sshFatal(s, git.ErrSystemMalfunction)
149			return
150		}
151	}
152}