internal/gitd/gitd.go
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}