clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

hold Git responses until git produces body bytes

Dami committed 7ab32ee
git-http-backend writes its CGI headers before reading the request. A
reverse proxy without full duplex HTTP/1.1 closes the unread client body
as soon as the backend responds, so a pack still streaming through the
proxy arrived truncated and receive-pack reported an unpacker error.
Wait for the first response body byte before writing headers, add a
regression test with a throttled half-duplex proxy, and enable full
duplex in the reference Caddyfile as well.
commit
7ab32ee7956634513df53eeac31647842a644b60
parent
573e6fba3dbc5a3dec10f9d51442835f44ff124f
author
Dami <dami.etoile@gmail.com>
3 files changed, +110 -0
Mdeploy/tiny.Caddyfile3+
Ainternal/daemon/git_proxy_push_test.go99++++++++++++++++++++++++++++++
Minternal/gitrelay/gitrelay.go8++
M deploy/tiny.Caddyfile +3 -0
A internal/daemon/git_proxy_push_test.go +99 -0
@@ -0,0 +1,99 @@
1+package daemon2+3+import (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 request21+// body as soon as the backend starts responding. git-http-backend writes its22+// headers before it reads the pack, so the relay must hold the response until23+// git produces body bytes or a large push arrives truncated at the proxy.24+func 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 = true34+ 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 is75+ // 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+91+type slowBody struct{ io.ReadCloser }92+93+func (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+}
M internal/gitrelay/gitrelay.go +8 -0