package githttp import ( "bytes" "compress/gzip" "errors" "fmt" "io" "net/http" "net/http/httptest" "strings" "testing" "github.com/go-git/go-git/v5/storage/memory" ) func TestInfoRefsBanner(t *testing.T) { rec := httptest.NewRecorder() if err := InfoRefs(rec, memory.NewStorage(), UploadPack); err != nil { t.Fatalf("InfoRefs: %v", err) } body := rec.Body.String() // pkt-line: 4-hex length ("001e") + "# service=git-upload-pack\n", then a // flush-pkt "0000". const want = "001e# service=git-upload-pack\n0000" if !strings.HasPrefix(body, want) { t.Fatalf("advertisement prefix = %q, want prefix %q", body[:min(len(body), 40)], want) } if ct := rec.Header().Get("Content-Type"); ct != "application/x-git-upload-pack-advertisement" { t.Errorf("Content-Type = %q", ct) } if cc := rec.Header().Get("Cache-Control"); cc != "no-cache" { t.Errorf("Cache-Control = %q", cc) } } func TestInfoRefsReceivePackCapabilities(t *testing.T) { rec := httptest.NewRecorder() if err := InfoRefs(rec, memory.NewStorage(), ReceivePack); err != nil { t.Fatalf("InfoRefs: %v", err) } body := rec.Body.String() if !strings.HasPrefix(body, "001f# service=git-receive-pack\n0000") { t.Fatalf("receive-pack banner wrong: %q", body[:min(len(body), 40)]) } // The advertisement must offer the capabilities stock git needs to push and // delete branches, and to receive a status report. for _, cap := range []string{"report-status", "delete-refs"} { if !strings.Contains(body, cap) { t.Errorf("advertisement missing capability %q", cap) } } } func TestRequestBodyGzip(t *testing.T) { var gz bytes.Buffer zw := gzip.NewWriter(&gz) const payload = "0009done\n" if _, err := zw.Write([]byte(payload)); err != nil { t.Fatal(err) } zw.Close() r := httptest.NewRequest("POST", "/x/git-upload-pack", bytes.NewReader(gz.Bytes())) r.Header.Set("Content-Encoding", "gzip") body, err := requestBody(r) if err != nil { t.Fatalf("requestBody: %v", err) } defer body.Close() got, err := io.ReadAll(body) if err != nil { t.Fatalf("read: %v", err) } if string(got) != payload { t.Fatalf("gunzipped body = %q, want %q", got, payload) } } // receivePackBody is a minimal create-branch request followed by payload. func receivePackBody(payload []byte) []byte { const ( oldOID = "0000000000000000000000000000000000000000" newOID = "1111111111111111111111111111111111111111" ) cmd := fmt.Sprintf("%s %s refs/heads/master\x00report-status\n", oldOID, newOID) var body bytes.Buffer fmt.Fprintf(&body, "%04x%s", len(cmd)+4, cmd) body.WriteString("0000") body.Write(payload) return body.Bytes() } func TestDecodeReceivePackProbe(t *testing.T) { r := httptest.NewRequest("POST", "/git-receive-pack", strings.NewReader("0000")) if _, err := DecodeReceivePack(r); !errors.Is(err, ErrProbe) { t.Fatalf("probe: err = %v, want ErrProbe", err) } // A real request still decodes, with its first pkt-line intact after the // 4-byte peek. r = httptest.NewRequest("POST", "/git-receive-pack", bytes.NewReader(receivePackBody([]byte("PACKDATA")))) req, err := DecodeReceivePack(r) if err != nil { t.Fatalf("real request: %v", err) } if len(req.Commands) != 1 || req.Commands[0].Name != "refs/heads/master" { t.Fatalf("commands = %+v", req.Commands) } rest, _ := io.ReadAll(req.Packfile) if string(rest) != "PACKDATA" { t.Errorf("pack stream = %q", rest) } // Empty and truncated bodies are errors, not probes. for _, body := range []string{"", "00"} { r = httptest.NewRequest("POST", "/git-receive-pack", strings.NewReader(body)) if _, err := DecodeReceivePack(r); err == nil || errors.Is(err, ErrProbe) { t.Errorf("body %q: err = %v", body, err) } } } func TestDecodeReceivePackProbeGzip(t *testing.T) { var gz bytes.Buffer zw := gzip.NewWriter(&gz) zw.Write([]byte("0000")) zw.Close() r := httptest.NewRequest("POST", "/git-receive-pack", &gz) r.Header.Set("Content-Encoding", "gzip") if _, err := DecodeReceivePack(r); !errors.Is(err, ErrProbe) { t.Fatalf("gzipped probe: err = %v, want ErrProbe", err) } } // TestDecodeReceivePackChunked drives a chunked body through a real // http.Server, which is what git sends once a request exceeds // http.postBuffer (default 1 MiB). Kept from docs/FR-large-push-400.md. func TestDecodeReceivePackChunked(t *testing.T) { var pack bytes.Buffer pack.WriteString("PACK") pack.Write([]byte{0, 0, 0, 2, 0, 0, 0, 0}) pack.Write(bytes.Repeat([]byte{0xAB}, 6<<20)) // past postBuffer pack.Write(make([]byte, 20)) body := receivePackBody(pack.Bytes()) errCh := make(chan error, 1) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.ContentLength != -1 { t.Errorf("ContentLength = %d, want -1 (chunked)", r.ContentLength) } req, err := DecodeReceivePack(r) if err == nil && req.Packfile != nil { _, err = io.Copy(io.Discard, req.Packfile) } errCh <- err })) defer srv.Close() hreq, _ := http.NewRequest(http.MethodPost, srv.URL+"/git-receive-pack", io.NopCloser(bytes.NewReader(body))) hreq.ContentLength = -1 // force chunked resp, err := http.DefaultClient.Do(hreq) if err != nil { t.Fatalf("request failed: %v", err) } resp.Body.Close() if err := <-errCh; err != nil { t.Fatalf("chunked body rejected: %v", err) } }