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