krz/gitbay

A CLI-first git forge.

clone: git clone https://gitbay.org/krz/gitbay.git

repo-descriptions: internal/gitd/gitd.go · raw

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