Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-09 15:49:16 -05:00
pkg-proxy/internal/handler/stale_cache_test.go
Simon Chrzanowski 2d258ddc4b Verify npm tarballs against dist.integrity
The proxy cached the first tarball upstream sent for a version without
checking it against the packument, so a tampered tarball was served to
every client after it.

- Pass the packument's dist.integrity to the existing integrity checks, so
  a fresh tarball is verified as it is stored or streamed; a mismatch is
  discarded and returns 502, or aborts the streamed response
- Record it with the cached version, so cache hits are verified against it
  through the same check as before
- Verify with integrity.Result.Verify, which accepts any digest of the
  strongest algorithm listed
- Fetch unverified as before when dist.integrity is missing or cannot be
  parsed, or the packument cannot be fetched
2026-10-14 15:40:36 +02:00

163 lines
5.4 KiB
Go

package handler
import (
"context"
"errors"
"io"
"strings"
"testing"
)
// These tests cover an upstream re-publishing a version: the cache holds one
// artifact and upstream now declares another digest for it.
const (
stalePkgPURL = "pkg:npm/pkg"
staleVersionPURL = "pkg:npm/pkg@1.0.0"
staleFilename = "pkg-1.0.0.tgz"
staleStoragePath = "npm/pkg/1.0.0/pkg-1.0.0.tgz"
staleURL = "https://registry.npmjs.org/pkg/-/pkg-1.0.0.tgz"
)
// seedCachedArtifact commits content the way a fetch does.
func seedCachedArtifact(t *testing.T, proxy *Proxy, store *mockStorage, content string) {
t.Helper()
ctx := context.Background()
if _, _, err := store.Store(ctx, staleStoragePath, strings.NewReader(content)); err != nil {
t.Fatalf("seeding storage: %v", err)
}
artifact := testArtifact(content, staleVersionPURL, staleFilename, "application/gzip")
if err := proxy.updateCacheDB("npm", "pkg", stalePkgPURL, staleURL, staleStoragePath, artifact, ""); err != nil {
t.Fatalf("seeding cache record: %v", err)
}
}
// cachedDigest reports the digest the cache record holds, or "" without one.
func cachedDigest(t *testing.T, proxy *Proxy) string {
t.Helper()
record, err := proxy.DB.GetCachedArtifact(stalePkgPURL, staleVersionPURL, staleFilename)
if err != nil {
t.Fatalf("reading cache record: %v", err)
}
if record == nil {
return ""
}
return record.Artifact.Digest.Encoded()
}
func bytesPresent(store *mockStorage) bool {
r, err := store.Open(context.Background(), staleStoragePath)
if err != nil {
return false
}
_ = r.Close()
return true
}
func TestStaleCacheCheckHasNoSideEffects(t *testing.T) {
proxy, _, store, _ := setupTestProxy(t)
seedCachedArtifact(t, proxy, store, "old bytes")
res, err := proxy.getCachedArtifactWithUpstreamHash(context.Background(),
stalePkgPURL, staleVersionPURL, staleFilename, sha256Hex("new bytes"))
if err != nil {
t.Fatalf("cache check failed: %v", err)
}
if res != nil {
drain(res)
t.Fatal("stale entry was served")
}
if got := cachedDigest(t, proxy); got != sha256Hex("old bytes") {
t.Errorf("record digest = %q, want the stale one kept: the check must not discard", got)
}
if !bytesPresent(store) {
t.Error("stale bytes were deleted by the check")
}
}
func TestStaleCacheIsDiscardedBeforeTheFetch(t *testing.T) {
proxy, _, store, fetcher := setupTestProxy(t)
seedCachedArtifact(t, proxy, store, "old bytes")
boom := errors.New("upstream unavailable")
fetcher.fetchErr = boom
_, err := proxy.GetOrFetchArtifactFromURLWithDigest(context.Background(),
"npm", "pkg", "1.0.0", staleFilename, staleURL, "sha256:"+sha256Hex("new bytes"))
if !errors.Is(err, boom) {
t.Fatalf("got %v, want the fetch failure", err)
}
if got := cachedDigest(t, proxy); got != "" {
t.Errorf("stale record survived a failed refresh, digest = %q", got)
}
// A request that read the stale record may still be opening its bytes,
// so they are queued for deletion rather than deleted.
if !bytesPresent(store) {
t.Error("stale bytes were deleted while a reader may still open them")
}
assertQueuedForDeletion(t, proxy.DB, staleStoragePath)
}
func TestStaleCacheIsReplacedByTheFetch(t *testing.T) {
proxy, _, store, fetcher := setupTestProxy(t)
seedCachedArtifact(t, proxy, store, "old bytes")
fetcher.artifact = artifactBody("new bytes")
upstream := sha256Hex("new bytes")
res, err := proxy.GetOrFetchArtifactFromURLWithDigest(context.Background(),
"npm", "pkg", "1.0.0", staleFilename, staleURL, "sha256:"+upstream)
if err != nil {
t.Fatalf("refresh failed: %v", err)
}
got, err := io.ReadAll(res.Reader)
_ = res.Reader.Close()
if err != nil || string(got) != "new bytes" {
t.Fatalf("got %q (err %v), want the refreshed bytes", got, err)
}
if !fetcher.fetchCalled {
t.Error("stale entry was served without a fetch")
}
if d := cachedDigest(t, proxy); d != upstream {
t.Errorf("record digest = %q, want %q", d, upstream)
}
fetcher.fetchCalled = false
res, err = proxy.GetOrFetchArtifactFromURLWithDigest(context.Background(),
"npm", "pkg", "1.0.0", staleFilename, staleURL, "sha256:"+upstream)
if err != nil {
t.Fatalf("request after refresh failed: %v", err)
}
drain(res)
if fetcher.fetchCalled || !res.Cached {
t.Errorf("request after refresh: fetched=%v cached=%v, want served from cache", fetcher.fetchCalled, res.Cached)
}
}
// TestLateLeaderKeepsRefreshedEntry is the race, at the point it would happen:
// a caller whose cache check saw a stale entry reaches the coalescing step
// after another caller's fetch replaced it. It must serve the replacement.
func TestLateLeaderKeepsRefreshedEntry(t *testing.T) {
proxy, _, store, fetcher := setupTestProxy(t)
seedCachedArtifact(t, proxy, store, "new bytes")
fetcher.fetchErr = errors.New("must not fetch")
upstream := sha256Hex("new bytes")
res, err := proxy.coalescedFetchFromURL(context.Background(),
"npm", "pkg", "1.0.0", staleFilename, stalePkgPURL, staleVersionPURL, staleURL, nil, upstream, "")
if err != nil {
t.Fatalf("late leader failed: %v", err)
}
got, err := io.ReadAll(res.Reader)
_ = res.Reader.Close()
if err != nil || string(got) != "new bytes" {
t.Fatalf("got %q (err %v), want the refreshed bytes", got, err)
}
if fetcher.fetchCalled {
t.Error("refreshed entry was fetched again")
}
if d := cachedDigest(t, proxy); d != upstream {
t.Errorf("refreshed record was discarded, digest = %q", d)
}
if !bytesPresent(store) {
t.Error("refreshed bytes were deleted")
}
}