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	// repo should be in the form of "repo.git"
 29	name := utils.SanitizeRepo(cmdLine[1])
 30	pk := s.PublicKey()
 31	ak := sshutils.MarshalAuthorizedKey(pk)
 32	user := proto.UserFromContext(ctx)
 33	accessLevel := be.AccessLevelForUser(ctx, name, user)
 34	// git bare repositories should end in ".git"
 35	// https://git-scm.com/docs/gitrepository-layout
 36	repoDir := name + ".git"
 37	reposDir := filepath.Join(cfg.DataPath, "repos")
 38	if err := git.EnsureWithin(reposDir, repoDir); err != nil {
 39		sshFatal(s, err)
 40		return
 41	}
 42
 43	// Set repo in context
 44	repo, _ := be.Repository(ctx, name)
 45	ctx.SetValue(proto.ContextKeyRepository, repo)
 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, repoDir),
 51		"SOFT_SERVE_PUBLIC_KEY=" + ak,
 52		"SOFT_SERVE_LOG_PATH=" + filepath.Join(cfg.DataPath, "log", "hooks.log"),
 53	}
 54
 55	if user != nil {
 56		envs = append(envs,
 57			"SOFT_SERVE_USERNAME="+user.Username(),
 58		)
 59	}
 60
 61	// Add ssh session & config environ
 62	envs = append(envs, s.Environ()...)
 63	envs = append(envs, cfg.Environ()...)
 64
 65	repoPath := filepath.Join(reposDir, repoDir)
 66	service := git.Service(cmdLine[0])
 67	cmd := git.ServiceCommand{
 68		Stdin:  s,
 69		Stdout: s,
 70		Stderr: s.Stderr(),
 71		Env:    envs,
 72		Dir:    repoPath,
 73	}
 74
 75	logger.Debug("git middleware", "cmd", service, "access", accessLevel.String())
 76
 77	switch service {
 78	case git.ReceivePackService:
 79		receivePackCounter.WithLabelValues(name).Inc()
 80		defer func() {
 81			receivePackSeconds.WithLabelValues(name).Add(time.Since(start).Seconds())
 82		}()
 83		if accessLevel < access.ReadWriteAccess {
 84			sshFatal(s, git.ErrNotAuthed)
 85			return
 86		}
 87		if repo == nil {
 88			if _, err := be.CreateRepository(ctx, name, user, proto.RepositoryOptions{Private: false}); err != nil {
 89				log.Errorf("failed to create repo: %s", err)
 90				sshFatal(s, err)
 91				return
 92			}
 93			createRepoCounter.WithLabelValues(name).Inc()
 94		}
 95
 96		if err := service.Handler(ctx, cmd); err != nil {
 97			sshFatal(s, git.ErrSystemMalfunction)
 98		}
 99
100		if err := git.EnsureDefaultBranch(ctx, cmd); err != nil {
101			sshFatal(s, git.ErrSystemMalfunction)
102		}
103
104		receivePackCounter.WithLabelValues(name).Inc()
105		return
106	case git.UploadPackService, git.UploadArchiveService:
107		if accessLevel < access.ReadOnlyAccess {
108			sshFatal(s, git.ErrNotAuthed)
109			return
110		}
111
112		switch service {
113		case git.UploadArchiveService:
114			uploadArchiveCounter.WithLabelValues(name).Inc()
115			defer func() {
116				uploadArchiveSeconds.WithLabelValues(name).Add(time.Since(start).Seconds())
117			}()
118		default:
119			uploadPackCounter.WithLabelValues(name).Inc()
120			defer func() {
121				uploadPackSeconds.WithLabelValues(name).Add(time.Since(start).Seconds())
122			}()
123		}
124
125		err := service.Handler(ctx, cmd)
126		if errors.Is(err, git.ErrInvalidRepo) {
127			sshFatal(s, git.ErrInvalidRepo)
128		} else if err != nil {
129			logger.Error("git middleware", "err", err)
130			sshFatal(s, git.ErrSystemMalfunction)
131		}
132
133		return
134	case git.LFSTransferService, git.LFSAuthenticateService:
135		if !cfg.LFS.Enabled {
136			return
137		}
138
139		if service == git.LFSTransferService && !cfg.LFS.SSHEnabled {
140			return
141		}
142
143		if accessLevel < access.ReadWriteAccess {
144			sshFatal(s, git.ErrNotAuthed)
145			return
146		}
147
148		if len(cmdLine) != 3 ||
149			(cmdLine[2] != lfs.OperationDownload && cmdLine[2] != lfs.OperationUpload) {
150			sshFatal(s, git.ErrInvalidRequest)
151			return
152		}
153
154		cmd.Args = []string{
155			name,
156			cmdLine[2],
157		}
158
159		if err := service.Handler(ctx, cmd); err != nil {
160			logger.Error("git middleware", "err", err)
161			sshFatal(s, git.ErrSystemMalfunction)
162			return
163		}
164	}
165}