tinyrelayfile
tinyrelay/internal/daemon/git_proxy_push_test.go/

internal/daemon/git_proxy_push_test.go raw
1package daemon
2
3import (
4 "context"
5 "crypto/rand"
6 "io"
7 "net/http/httptest"
8 "net/http/httputil"
9 "net/url"
10 "os"
11 "path/filepath"
12 "strings"
13 "testing"
14 "time"
15
16 "github.com/FelineStateMachine/tinyrelay/internal/event"
17 "github.com/FelineStateMachine/tinyrelay/internal/relay"
18)
19
20// A reverse proxy without full duplex HTTP/1.1 discards the unread request
21// body as soon as the backend starts responding. git-http-backend writes its
22// headers before it reads the pack, so the relay must hold the response until
23// git produces body bytes or a large push arrives truncated at the proxy.
24func TestLargePushSurvivesHalfDuplexReverseProxy(t *testing.T) {
25 ctx := context.Background()
26 secret := strings.Repeat("0", 63) + "1"
27 owner, err := event.PublicKey(secret)
28 if err != nil {
29 t.Fatal(err)
30 }
31 app, tenant := testTenant(t)
32 p := tenant.Policy()
33 p.Features.Grasp = true
34 if err := tenant.applyPolicy(ctx, p); err != nil {
35 t.Fatal(err)
36 }
37 const ownerNPub = "npub10xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqpkge6d"
38 announcement := event.Event{Kind: event.KIND_REPO, CreatedAt: time.Now().Unix(), Tags: [][]string{{"d", "big"}, {"clone", "http://relay.test/" + ownerNPub + "/big.git"}, {"relays", "ws://relay.test"}}}
39 if err := event.Sign(&announcement, secret); err != nil {
40 t.Fatal(err)
41 }
42 if _, err := tenant.router.Publish(ctx, announcement, relay.Session{PubKeys: []string{owner}}); err != nil {
43 t.Fatal(err)
44 }
45
46 work := filepath.Join(t.TempDir(), "work")
47 gitTest(t, "init", "-b", "main", work)
48 gitTest(t, "-C", work, "config", "user.email", "test@example.com")
49 gitTest(t, "-C", work, "config", "user.name", "Test")
50 blob := make([]byte, 1<<20)
51 if _, err := rand.Read(blob); err != nil {
52 t.Fatal(err)
53 }
54 if err := os.WriteFile(filepath.Join(work, "blob.bin"), blob, 0o600); err != nil {
55 t.Fatal(err)
56 }
57 gitTest(t, "-C", work, "add", "blob.bin")
58 gitTest(t, "-C", work, "commit", "-m", "big")
59 sha := strings.TrimSpace(gitOutput(t, "-C", work, "rev-parse", "HEAD"))
60 state := event.Event{Kind: event.KIND_REPO_STATE, CreatedAt: time.Now().Unix(), Tags: [][]string{{"d", "big"}, {"HEAD", "ref: refs/heads/main"}, {"refs/heads/main", sha}}}
61 if err := event.Sign(&state, secret); err != nil {
62 t.Fatal(err)
63 }
64 if _, err := tenant.router.Publish(ctx, state, relay.Session{PubKeys: []string{owner}}); err != nil {
65 t.Fatal(err)
66 }
67
68 backend := httptest.NewServer(app)
69 defer backend.Close()
70 target, err := url.Parse(backend.URL)
71 if err != nil {
72 t.Fatal(err)
73 }
74 // Throttle the upload so the backend's headers arrive while the pack is
75 // still streaming, as they do over a real network.
76 proxy := httptest.NewServer(&httputil.ReverseProxy{Rewrite: func(request *httputil.ProxyRequest) {
77 request.SetURL(target)
78 request.Out.Body = &slowBody{ReadCloser: request.In.Body}
79 }})
80 defer proxy.Close()
81
82 // A small post buffer makes git stream the pack as a chunked HTTP/1.1 body.
83 gitTest(t, "-C", work, "-c", "http.postBuffer=1024", "-c", "http.version=HTTP/1.1", "push", proxy.URL+"/r/main/"+ownerNPub+"/big.git", "refs/heads/main:refs/heads/main")
84 clone := filepath.Join(t.TempDir(), "clone")
85 gitTest(t, "clone", proxy.URL+"/r/main/"+ownerNPub+"/big.git", clone)
86 if got := strings.TrimSpace(gitOutput(t, "-C", clone, "rev-parse", "HEAD")); got != sha {
87 t.Fatalf("clone HEAD = %s, want %s", got, sha)
88 }
89}
90
91type slowBody struct{ io.ReadCloser }
92
93func (b *slowBody) Read(p []byte) (int, error) {
94 if len(p) > 4096 {
95 p = p[:4096]
96 }
97 time.Sleep(2 * time.Millisecond)
98 return b.ReadCloser.Read(p)
99}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs