1package server
  2
  3import (
  4	"errors"
  5	"fmt"
  6	"io"
  7	"os"
  8	"path/filepath"
  9	"strings"
 10
 11	"github.com/charmbracelet/log"
 12	"github.com/charmbracelet/soft-serve/git"
 13	"github.com/go-git/go-git/v5/plumbing/format/pktline"
 14)
 15
 16var (
 17
 18	// ErrNotAuthed represents unauthorized access.
 19	ErrNotAuthed = errors.New("you are not authorized to do this")
 20
 21	// ErrSystemMalfunction represents a general system error returned to clients.
 22	ErrSystemMalfunction = errors.New("something went wrong")
 23
 24	// ErrInvalidRepo represents an attempt to access a non-existent repo.
 25	ErrInvalidRepo = errors.New("invalid repo")
 26
 27	// ErrInvalidRequest represents an invalid request.
 28	ErrInvalidRequest = errors.New("invalid request")
 29
 30	// ErrMaxConnections represents a maximum connection limit being reached.
 31	ErrMaxConnections = errors.New("too many connections, try again later")
 32
 33	// ErrTimeout is returned when the maximum read timeout is exceeded.
 34	ErrTimeout = errors.New("I/O timeout reached")
 35)
 36
 37// Git protocol commands.
 38const (
 39	ReceivePackBin   = "git-receive-pack"
 40	UploadPackBin    = "git-upload-pack"
 41	UploadArchiveBin = "git-upload-archive"
 42)
 43
 44// UploadPack runs the git upload-pack protocol against the provided repo.
 45func UploadPack(in io.Reader, out io.Writer, er io.Writer, repoDir string) error {
 46	exists, err := fileExists(repoDir)
 47	if !exists {
 48		return ErrInvalidRepo
 49	}
 50	if err != nil {
 51		return err
 52	}
 53	return RunGit(in, out, er, "", UploadPackBin[4:], repoDir)
 54}
 55
 56// UploadArchive runs the git upload-archive protocol against the provided repo.
 57func UploadArchive(in io.Reader, out io.Writer, er io.Writer, repoDir string) error {
 58	exists, err := fileExists(repoDir)
 59	if !exists {
 60		return ErrInvalidRepo
 61	}
 62	if err != nil {
 63		return err
 64	}
 65	return RunGit(in, out, er, "", UploadArchiveBin[4:], repoDir)
 66}
 67
 68// ReceivePack runs the git receive-pack protocol against the provided repo.
 69func ReceivePack(in io.Reader, out io.Writer, er io.Writer, repoDir string) error {
 70	if err := ensureRepo(repoDir, ""); err != nil {
 71		return err
 72	}
 73	if err := RunGit(in, out, er, "", ReceivePackBin[4:], repoDir); err != nil {
 74		return err
 75	}
 76	return ensureDefaultBranch(in, out, er, repoDir)
 77}
 78
 79// RunGit runs a git command in the given repo.
 80func RunGit(in io.Reader, out io.Writer, err io.Writer, dir string, args ...string) error {
 81	c := git.NewCommand(args...)
 82	return c.RunInDirWithOptions(dir, git.RunInDirOptions{
 83		Stdin:  in,
 84		Stdout: out,
 85		Stderr: err,
 86	})
 87}
 88
 89// WritePktline encodes and writes a pktline to the given writer.
 90func WritePktline(w io.Writer, v ...interface{}) {
 91	msg := fmt.Sprintln(v...)
 92	pkt := pktline.NewEncoder(w)
 93	if err := pkt.EncodeString(msg); err != nil {
 94		log.Debugf("git: error writing pkt-line message: %s", err)
 95	}
 96	if err := pkt.Flush(); err != nil {
 97		log.Debugf("git: error flushing pkt-line message: %s", err)
 98	}
 99}
100
101// ensureWithin ensures the given repo is within the repos directory.
102func ensureWithin(reposDir string, repo string) error {
103	repoDir := filepath.Join(reposDir, repo)
104	absRepos, err := filepath.Abs(reposDir)
105	if err != nil {
106		log.Debugf("failed to get absolute path for repo: %s", err)
107		return ErrSystemMalfunction
108	}
109	absRepo, err := filepath.Abs(repoDir)
110	if err != nil {
111		log.Debugf("failed to get absolute path for repos: %s", err)
112		return ErrSystemMalfunction
113	}
114
115	// ensure the repo is within the repos directory
116	if !strings.HasPrefix(absRepo, absRepos) {
117		log.Debugf("repo path is outside of repos directory: %s", absRepo)
118		return ErrInvalidRepo
119	}
120
121	return nil
122}
123
124func fileExists(path string) (bool, error) {
125	_, err := os.Stat(path)
126	if err == nil {
127		return true, nil
128	}
129	if os.IsNotExist(err) {
130		return false, nil
131	}
132	return true, err
133}
134
135func ensureRepo(dir string, repo string) error {
136	exists, err := fileExists(dir)
137	if err != nil {
138		return err
139	}
140	if !exists {
141		err = os.MkdirAll(dir, os.ModeDir|os.FileMode(0700))
142		if err != nil {
143			return err
144		}
145	}
146	rp := filepath.Join(dir, repo)
147	exists, err = fileExists(rp)
148	if err != nil {
149		return err
150	}
151	// FIXME: use backend.CreateRepository
152	if !exists {
153		_, err := git.Init(rp, true)
154		if err != nil {
155			return err
156		}
157	}
158	return nil
159}
160
161func ensureDefaultBranch(in io.Reader, out io.Writer, er io.Writer, repoPath string) error {
162	r, err := git.Open(repoPath)
163	if err != nil {
164		return err
165	}
166	brs, err := r.Branches()
167	if err != nil {
168		return err
169	}
170	if len(brs) == 0 {
171		return fmt.Errorf("no branches found")
172	}
173	// Rename the default branch to the first branch available
174	_, err = r.HEAD()
175	if err == git.ErrReferenceNotExist {
176		// FIXME: use backend.SetDefaultBranch
177		err = RunGit(in, out, er, repoPath, "branch", "-M", brs[0])
178		if err != nil {
179			return err
180		}
181	}
182	if err != nil && err != git.ErrReferenceNotExist {
183		return err
184	}
185	return nil
186}