internal/gitd/gitd.go

dd06e80a071d451ebb7b083363e0580550481114
gitbay/internal/gitd/gitd.go history · blame · raw

102 lines · 2525 bytes

  1// Package gitd implements the anonymous git:// protocol listener. Read-only
  2// upload-pack, and only for repositories that are public AND have opted in
  3// via settings — on an instance where [git_daemon] is enabled at all.
  4package gitd
  5
  6import (
  7	"fmt"
  8	"io"
  9	"net"
 10	"os"
 11	"os/exec"
 12	"strconv"
 13	"strings"
 14	"time"
 15
 16	"gitbay.org/gitbay/internal/config"
 17	"gitbay.org/gitbay/internal/control"
 18	"gitbay.org/gitbay/internal/store"
 19	"gitbay.org/gitbay/internal/toolpath"
 20)
 21
 22type Server struct {
 23	cfg config.Config
 24	st  *store.Store
 25}
 26
 27func New(cfg config.Config, st *store.Store) *Server { return &Server{cfg: cfg, st: st} }
 28
 29func (s *Server) Serve(ln net.Listener) error {
 30	for {
 31		conn, err := ln.Accept()
 32		if err != nil {
 33			return err
 34		}
 35		go s.handle(conn)
 36	}
 37}
 38
 39func (s *Server) handle(conn net.Conn) {
 40	defer conn.Close()
 41	conn.SetReadDeadline(time.Now().Add(30 * time.Second))
 42
 43	req, err := readPktLine(conn)
 44	if err != nil {
 45		return
 46	}
 47	conn.SetReadDeadline(time.Time{})
 48
 49	// Request form: "git-upload-pack /owner/name.git\0host=...\0[\0extra\0]"
 50	service, rest, ok := strings.Cut(req, " ")
 51	if !ok || service != "git-upload-pack" {
 52		writeErr(conn, "only git-upload-pack is available over git://")
 53		return
 54	}
 55	parts := strings.Split(rest, "\x00")
 56	path := parts[0]
 57	var protoEnv []string
 58	for _, p := range parts[1:] {
 59		if v, ok := strings.CutPrefix(p, "version="); ok {
 60			protoEnv = []string{"GIT_PROTOCOL=version=" + v}
 61		}
 62	}
 63
 64	repo, err := s.st.RepoByPath(path)
 65	if err != nil || repo.Visibility != "public" || !repo.Settings.GitDaemon {
 66		// One answer for missing, private, and not-opted-in.
 67		writeErr(conn, "repository not exported")
 68		return
 69	}
 70
 71	dir := control.RepoDir(s.cfg.Server.Root, repo.OwnerName, repo.Name)
 72	cmd := exec.Command(toolpath.Look("git"), "upload-pack", dir)
 73	cmd.Env = append(os.Environ(), protoEnv...)
 74	cmd.Stdin = conn
 75	cmd.Stdout = conn
 76	cmd.Stderr = io.Discard
 77	cmd.Run()
 78}
 79
 80func readPktLine(r io.Reader) (string, error) {
 81	var lenHex [4]byte
 82	if _, err := io.ReadFull(r, lenHex[:]); err != nil {
 83		return "", err
 84	}
 85	n, err := strconv.ParseUint(string(lenHex[:]), 16, 16)
 86	if err != nil || n < 4 || n > 65520 {
 87		return "", fmt.Errorf("bad pkt length %q", lenHex)
 88	}
 89	if n == 4 {
 90		return "", nil // flush-pkt
 91	}
 92	buf := make([]byte, n-4)
 93	if _, err := io.ReadFull(r, buf); err != nil {
 94		return "", err
 95	}
 96	return strings.TrimSuffix(string(buf), "\n"), nil
 97}
 98
 99func writeErr(w io.Writer, msg string) {
100	line := "ERR " + msg + "\n"
101	fmt.Fprintf(w, "%04x%s", len(line)+4, line)
102}