fix(deb): normalize apt flat-repo ./ dist prefix in github_deb serve
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful

The metadata-only github_deb flat repo has the same defect as the local repo:
real apt requests <repo>/./Packages (and ./Release), which ServeRemote matched
literally and 404d. Reuse deb.go normalizeIndexPath to collapse the dot-segment
before matching the synthesized index.
This commit is contained in:
unkin-agent
2026-08-11 22:41:55 +10:00
committed by unkin-agent
parent b2a6be8eb5
commit fd4fb160df
2 changed files with 47 additions and 1 deletions
+5 -1
View File
@@ -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)
+42
View File
@@ -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) {