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	// repo should be in the form of "repo.git"
 28	name := utils.SanitizeRepo(cmdLine[1])
 29	pk := s.PublicKey()
 30	ak := sshutils.MarshalAuthorizedKey(pk)
 31	accessLevel := be.AccessLevelByPublicKey(ctx, name, pk)
 32	// git bare repositories should end in ".git"
 33	// https://git-scm.com/docs/gitrepository-layout
 34	repo := name + ".git"
 35	reposDir := filepath.Join(cfg.DataPath, "repos")
 36	if err := git.EnsureWithin(reposDir, repo); err != nil {
 37		sshFatal(s, err)
 38		return
 39	}
 40
 41	// Environment variables to pass down to git hooks.
 42	envs := []string{
 43		"SOFT_SERVE_REPO_NAME=" + name,
 44		"SOFT_SERVE_REPO_PATH=" + filepath.Join(reposDir, repo),
 45		"SOFT_SERVE_PUBLIC_KEY=" + ak,
 46		"SOFT_SERVE_USERNAME=" + s.User(),
 47		"SOFT_SERVE_LOG_PATH=" + filepath.Join(cfg.DataPath, "log", "hooks.log"),
 48	}
 49
 50	// Add ssh session & config environ
 51	envs = append(envs, s.Environ()...)
 52	envs = append(envs, cfg.Environ()...)
 53
 54	repoDir := filepath.Join(reposDir, repo)
 55	service := git.Service(cmdLine[0])
 56	cmd := git.ServiceCommand{
 57		Stdin:  s,
 58		Stdout: s,
 59		Stderr: s.Stderr(),
 60		Env:    envs,
 61		Dir:    repoDir,
 62	}
 63
 64	logger.Debug("git middleware", "cmd", service, "access", accessLevel.String())
 65
 66	switch service {
 67	case git.ReceivePackService:
 68		receivePackCounter.WithLabelValues(name).Inc()
 69		defer func() {
 70			receivePackSeconds.WithLabelValues(name).Add(time.Since(start).Seconds())
 71		}()
 72		if accessLevel < access.ReadWriteAccess {
 73			sshFatal(s, git.ErrNotAuthed)
 74			return
 75		}
 76		if _, err := be.Repository(ctx, name); err != nil {
 77			if _, err := be.CreateRepository(ctx, name, proto.RepositoryOptions{Private: false}); err != nil {
 78				log.Errorf("failed to create repo: %s", err)
 79				sshFatal(s, err)
 80				return
 81			}
 82			createRepoCounter.WithLabelValues(name).Inc()
 83		}
 84
 85		if err := git.ReceivePack(ctx, cmd); err != nil {
 86			sshFatal(s, git.ErrSystemMalfunction)
 87		}
 88
 89		if err := git.EnsureDefaultBranch(ctx, cmd); err != nil {
 90			sshFatal(s, git.ErrSystemMalfunction)
 91		}
 92
 93		receivePackCounter.WithLabelValues(name).Inc()
 94		return
 95	case git.UploadPackService, git.UploadArchiveService:
 96		if accessLevel < access.ReadOnlyAccess {
 97			sshFatal(s, git.ErrNotAuthed)
 98			return
 99		}
100
101		handler := git.UploadPack
102		switch service {
103		case git.UploadArchiveService:
104			handler = git.UploadArchive
105			uploadArchiveCounter.WithLabelValues(name).Inc()
106			defer func() {
107				uploadArchiveSeconds.WithLabelValues(name).Add(time.Since(start).Seconds())
108			}()
109		default:
110			uploadPackCounter.WithLabelValues(name).Inc()
111			defer func() {
112				uploadPackSeconds.WithLabelValues(name).Add(time.Since(start).Seconds())
113			}()
114		}
115
116		err := handler(ctx, cmd)
117		if errors.Is(err, git.ErrInvalidRepo) {
118			sshFatal(s, git.ErrInvalidRepo)
119		} else if err != nil {
120			logger.Error("git middleware", "err", err)
121			sshFatal(s, git.ErrSystemMalfunction)
122		}
123	}
124}