From fd4fb160df6d1eaa38117c9bd44b45b2d5766f78 Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Tue, 11 Aug 2026 22:41:55 +1000 Subject: [PATCH] fix(deb): normalize apt flat-repo ./ dist prefix in github_deb serve The metadata-only github_deb flat repo has the same defect as the local repo: real apt requests /./Packages (and ./Release), which ServeRemote matched literally and 404d. Reuse deb.go normalizeIndexPath to collapse the dot-segment before matching the synthesized index. --- internal/provider/deb/github.go | 6 +++- internal/provider/deb/github_test.go | 42 ++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/internal/provider/deb/github.go b/internal/provider/deb/github.go index 518249d..3c8fcad 100644 --- a/internal/provider/deb/github.go +++ b/internal/provider/deb/github.go @@ -131,9 +131,13 @@ func (p *GitHubProvider) AuthHeaders(ctx context.Context, remote models.Remote) // (Packages/Packages.gz/Release), 404s the signed index variants (the repo is // consumed via [trusted=yes]), and 302-redirects .deb downloads to the backend // releases_remote. Returns false only for paths it does not own. -func (p *GitHubProvider) ServeRemote(w http.ResponseWriter, r *http.Request, remote models.Remote, path, proxyBaseURL string, store provider.RemoteMetadataStore) bool { +func (p *GitHubProvider) ServeRemote(w http.ResponseWriter, r *http.Request, remote models.Remote, reqPath, proxyBaseURL string, store provider.RemoteMetadataStore) bool { p.onRequest(remote, store) + // apt appends the flat-repo dist "./" verbatim, so it asks for "./Packages" + // etc.; collapse the dot-segment before matching the synthesized index. + path := normalizeIndexPath(reqPath) + switch path { case "Packages", "Packages.gz", "Release": p.serveIndex(w, r, remote, path, store) diff --git a/internal/provider/deb/github_test.go b/internal/provider/deb/github_test.go index ef6580c..f27fbc7 100644 --- a/internal/provider/deb/github_test.go +++ b/internal/provider/deb/github_test.go @@ -1,6 +1,7 @@ package deb import ( + "bytes" "compress/gzip" "context" "crypto/sha256" @@ -333,6 +334,47 @@ func TestGitHubServeRemoteIndexAndRedirect(t *testing.T) { } } +// Real apt appends the flat-repo dist "./" verbatim, so the metadata-only remote +// receives "./Packages" / "./Release"; ServeRemote must collapse the dot-segment +// and synthesize the same index as the un-prefixed request. +func TestGitHubServeRemoteAptDotSegment(t *testing.T) { + fx := newGitHubFixture(t, true) + p := newTestProvider() + store := newFakeStore() + remote := fx.remote() + const proxyBase = "https://artifactapi.example" + + serve := func(path string) *httptest.ResponseRecorder { + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/remote/acme-deb/"+path, nil) + if !p.ServeRemote(rec, req, remote, path, proxyBase, store) { + t.Fatalf("ServeRemote did not handle %q", path) + } + return rec + } + + // Packages is deterministic: byte-identical to the un-prefixed request. + plain, dotted := serve("Packages"), serve("./Packages") + if plain.Code != 200 || dotted.Code != 200 { + t.Fatalf("Packages: plain=%d dotted=%d, want 200/200", plain.Code, dotted.Code) + } + if !strings.Contains(dotted.Body.String(), "Package: demo") { + t.Fatalf("./Packages missing synthesized body: %s", dotted.Body.String()) + } + if !bytes.Equal(plain.Body.Bytes(), dotted.Body.Bytes()) { + t.Error("./Packages body differs from Packages body") + } + + // Release carries a time.Now() Date: header; compare the rest. + rPlain, rDotted := serve("Release"), serve("./Release") + if rPlain.Code != 200 || rDotted.Code != 200 { + t.Fatalf("Release: plain=%d dotted=%d, want 200/200", rPlain.Code, rDotted.Code) + } + if stripDate(rPlain.Body.String()) != stripDate(rDotted.Body.String()) { + t.Error("./Release body differs from Release body (ignoring Date)") + } +} + // A canceled inbound request must still serve the warm cache (detached context), // not turn the metadata read into a 500. func TestGitHubServeRemoteCanceledRequestServesCache(t *testing.T) {