|
| 1 | +// package servegit provides a smart Git HTTP transfer protocol handler. |
| 2 | +package servegit |
| 3 | + |
| 4 | +import ( |
| 5 | +"bytes" |
| 6 | +"compress/gzip" |
| 7 | +"context" |
| 8 | +"net/http" |
| 9 | +"os" |
| 10 | +"os/exec" |
| 11 | +"strconv" |
| 12 | +"strings" |
| 13 | + |
| 14 | +"slices" |
| 15 | + |
| 16 | +"github.com/sourcegraph/sourcegraph/lib/errors" |
| 17 | +) |
| 18 | + |
| 19 | +var uploadPackArgs = []string{ |
| 20 | +// Partial clones/fetches |
| 21 | +"-c", "uploadpack.allowFilter=true", |
| 22 | + |
| 23 | +// Can fetch any object. Used in case of race between a resolve ref and a |
| 24 | +// fetch of a commit. Safe to do, since this is only used internally. |
| 25 | +"-c", "uploadpack.allowAnySHA1InWant=true", |
| 26 | + |
| 27 | +// The maximum size of memory that is consumed by each thread in git-pack-objects[1] |
| 28 | +// for pack window memory when no limit is given on the command line. |
| 29 | +// |
| 30 | +// Important for large monorepos to not run into memory issues when cloned. |
| 31 | +"-c", "pack.windowMemory=100m", |
| 32 | + |
| 33 | +"upload-pack", |
| 34 | + |
| 35 | +"--stateless-rpc", "--strict", |
| 36 | +} |
| 37 | + |
| 38 | +// Handler is a smart Git HTTP transfer protocol as documented at |
| 39 | +// https://www.git-scm.com/docs/http-protocol. |
| 40 | +// |
| 41 | +// This allows users to clone any git repo. We only support the smart |
| 42 | +// protocol. We aim to support modern git features such as protocol v2 to |
| 43 | +// minimize traffic. |
| 44 | +type Handler struct { |
| 45 | +// Dir is a function which takes a repository name and returns an absolute |
| 46 | +// path to the GIT_DIR for it. |
| 47 | +Dir func(context.Context, string) (string, error) |
| 48 | + |
| 49 | +// ErrorHook is called if we fail to run the git command. The main use of |
| 50 | +// this is to inject logging. For example in src-cli we don't use |
| 51 | +// sourcegraph/log so this allows us to use stdlib log. |
| 52 | +// |
| 53 | +// Note: This is required to be set |
| 54 | +ErrorHook func(err error, stderr string) |
| 55 | + |
| 56 | +// CommandHook if non-nil will run with the git upload command before we |
| 57 | +// start the command. |
| 58 | +// |
| 59 | +// This allows the command to be modified before running. In practice |
| 60 | +// sourcegraph.com will add a flowrated writer for Stdout to treat our |
| 61 | +// internal networks more kindly. |
| 62 | +CommandHook func(*exec.Cmd) |
| 63 | + |
| 64 | +// Trace if non-nil is called at the start of serving a request. It will |
| 65 | +// call the returned function when done executing. If the executation |
| 66 | +// failed, it will pass in a non-nil error. |
| 67 | +Trace func(ctx context.Context, svc, repo, protocol string) func(error) |
| 68 | +} |
| 69 | + |
| 70 | +func (s *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
| 71 | +ctx := r.Context() |
| 72 | + |
| 73 | +// Only support clones and fetches (git upload-pack). /info/refs sets the |
| 74 | +// service field. |
| 75 | +if svcQ := r.URL.Query().Get("service"); svcQ != "" && svcQ != "git-upload-pack" { |
| 76 | +http.Error(w, "only support service git-upload-pack", http.StatusBadRequest) |
| 77 | +return |
| 78 | +} |
| 79 | + |
| 80 | +var repo, svc string |
| 81 | +for _, suffix := range []string{"/info/refs", "/git-upload-pack"} { |
| 82 | +if strings.HasSuffix(r.URL.Path, suffix) { |
| 83 | +svc = suffix |
| 84 | +repo = strings.TrimSuffix(r.URL.Path, suffix) |
| 85 | +repo = strings.TrimPrefix(repo, "/") |
| 86 | +break |
| 87 | +} |
| 88 | +} |
| 89 | + |
| 90 | +dir, err := s.Dir(ctx, repo) |
| 91 | +if err != nil { |
| 92 | +http.Error(w, "failed to determine repository path: "+err.Error(), http.StatusInternalServerError) |
| 93 | +return |
| 94 | +} |
| 95 | + |
| 96 | +if _, err = os.Stat(dir); os.IsNotExist(err) { |
| 97 | +http.Error(w, "repository not found", http.StatusNotFound) |
| 98 | +return |
| 99 | +} else if err != nil { |
| 100 | +http.Error(w, "failed to stat repo: "+err.Error(), http.StatusInternalServerError) |
| 101 | +return |
| 102 | +} |
| 103 | + |
| 104 | +body := r.Body |
| 105 | +defer body.Close() |
| 106 | + |
| 107 | +if r.Header.Get("Content-Encoding") == "gzip" { |
| 108 | +gzipReader, err := gzip.NewReader(body) |
| 109 | +if err != nil { |
| 110 | +http.Error(w, "malformed payload: "+err.Error(), http.StatusBadRequest) |
| 111 | +return |
| 112 | +} |
| 113 | +defer gzipReader.Close() |
| 114 | + |
| 115 | +body = gzipReader |
| 116 | +} |
| 117 | + |
| 118 | +// err is set if we fail to run command or have an unexpected svc. It is |
| 119 | +// captured for tracing. |
| 120 | +if s.Trace != nil { |
| 121 | +done := s.Trace(ctx, svc, repo, r.Header.Get("Git-Protocol")) |
| 122 | +defer func() { |
| 123 | +done(err) |
| 124 | +}() |
| 125 | +} |
| 126 | + |
| 127 | +args := slices.Clone(uploadPackArgs) |
| 128 | +switch svc { |
| 129 | +case "/info/refs": |
| 130 | +w.Header().Set("Content-Type", "application/x-git-upload-pack-advertisement") |
| 131 | +_, _ = w.Write(packetWrite("# service=git-upload-pack\n")) |
| 132 | +_, _ = w.Write([]byte("0000")) |
| 133 | +args = append(args, "--advertise-refs") |
| 134 | +case "/git-upload-pack": |
| 135 | +w.Header().Set("Content-Type", "application/x-git-upload-pack-result") |
| 136 | +default: |
| 137 | +err = errors.Errorf("unexpected subpath (want /info/refs or /git-upload-pack): %q", svc) |
| 138 | +http.Error(w, err.Error(), http.StatusInternalServerError) |
| 139 | +return |
| 140 | +} |
| 141 | +args = append(args, dir) |
| 142 | + |
| 143 | +env := os.Environ() |
| 144 | +if protocol := r.Header.Get("Git-Protocol"); protocol != "" { |
| 145 | +env = append(env, "GIT_PROTOCOL="+protocol) |
| 146 | +} |
| 147 | + |
| 148 | +var stderr bytes.Buffer |
| 149 | +cmd := exec.CommandContext(ctx, "git", args...) |
| 150 | +cmd.Env = env |
| 151 | +cmd.Stdout = w |
| 152 | +cmd.Stderr = &stderr |
| 153 | +cmd.Stdin = body |
| 154 | + |
| 155 | +if s.CommandHook != nil { |
| 156 | +s.CommandHook(cmd) |
| 157 | +} |
| 158 | + |
| 159 | +err = cmd.Run() |
| 160 | +if err != nil { |
| 161 | +err = errors.Errorf("error running git service command args=%q: %w", args, err) |
| 162 | +s.ErrorHook(err, stderr.String()) |
| 163 | +_, _ = w.Write([]byte("\n" + err.Error() + "\n")) |
| 164 | +} |
| 165 | +} |
| 166 | + |
| 167 | +func packetWrite(str string) []byte { |
| 168 | +s := strconv.FormatInt(int64(len(str)+4), 16) |
| 169 | +if len(s)%4 != 0 { |
| 170 | +s = strings.Repeat("0", 4-len(s)%4) + s |
| 171 | +} |
| 172 | +return []byte(s + str) |
| 173 | +} |
0 commit comments