krz/gitbay
A CLI-first git forge.
clone: git clone https://gitbay.org/krz/gitbay.git
main: 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}