Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/aws/aws-sdk-go-v2/config v1.32.38
github.com/aws/aws-sdk-go-v2/service/ecr v1.61.0
github.com/git-pkgs/archives v0.5.1
github.com/git-pkgs/artifacts v0.2.0
github.com/git-pkgs/cooldown v0.2.0
github.com/git-pkgs/enrichment v0.7.0
github.com/git-pkgs/gcs v0.1.0
Expand All @@ -21,6 +22,7 @@ require (
github.com/go-chi/chi/v5 v5.3.2
github.com/jmoiron/sqlx v1.4.0
github.com/lib/pq v1.12.3
github.com/opencontainers/go-digest v1.0.0
github.com/prometheus/client_golang v1.24.1
github.com/prometheus/client_model v0.6.2
github.com/spdx/tools-golang v0.5.7
Expand Down Expand Up @@ -132,7 +134,6 @@ require (
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fzipp/gocyclo v0.6.0 // indirect
github.com/ghostiam/protogetter v0.3.21 // indirect
github.com/git-pkgs/artifacts v0.2.0 // indirect
github.com/git-pkgs/packageurl-go v0.3.1 // indirect
github.com/git-pkgs/pom v0.1.7 // indirect
github.com/github/go-spdx/v2 v2.7.0 // indirect
Expand Down Expand Up @@ -228,7 +229,6 @@ require (
github.com/nunnatsa/ginkgolinter v0.24.0 // indirect
github.com/oapi-codegen/nullable v1.2.0 // indirect
github.com/oapi-codegen/runtime v1.6.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/package-url/packageurl-go v0.1.7 // indirect
github.com/pandatix/go-cvss v0.6.2 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
Expand Down
18 changes: 12 additions & 6 deletions internal/database/database_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,14 +279,20 @@ func TestGetCachedArtifact(t *testing.T) {
if cached.StoragePath != "/cache/npm/"+filename {
t.Errorf("expected cached storage path, got %q", cached.StoragePath)
}
if cached.ContentHash.String != testContentHash {
t.Errorf("expected cached content hash, got %q", cached.ContentHash.String)
if cached.Artifact.PURL != versionPURL {
t.Errorf("expected cached PURL %q, got %q", versionPURL, cached.Artifact.PURL)
}
if cached.Size.Int64 != 12345 {
t.Errorf("expected cached size 12345, got %d", cached.Size.Int64)
if cached.Artifact.Digest.String() != "sha256:"+testContentHash {
t.Errorf("expected cached digest, got %q", cached.Artifact.Digest)
}
if cached.ContentType.String != "application/gzip" {
t.Errorf("expected cached content type, got %q", cached.ContentType.String)
if cached.Artifact.Size != 12345 {
t.Errorf("expected cached size 12345, got %d", cached.Artifact.Size)
}
if cached.Artifact.Filename != filename {
t.Errorf("expected cached filename %q, got %q", filename, cached.Artifact.Filename)
}
if cached.Artifact.MediaType != "application/gzip" {
t.Errorf("expected cached content type, got %q", cached.Artifact.MediaType)
}
if cached.Integrity.String != testIntegrity {
t.Errorf("expected cached integrity, got %q", cached.Integrity.String)
Expand Down
37 changes: 34 additions & 3 deletions internal/database/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import (
"database/sql"
"fmt"
"time"

"github.com/git-pkgs/artifacts"
"github.com/opencontainers/go-digest"
)

// Package queries
Expand Down Expand Up @@ -225,7 +228,7 @@ func (db *DB) GetArtifact(versionPURL, filename string) (*Artifact, error) {

// GetCachedArtifact returns the fields needed to serve a cached artifact.
func (db *DB) GetCachedArtifact(packagePURL, versionPURL, filename string) (*CachedArtifact, error) {
var artifact CachedArtifact
var row cachedArtifactRow
query := db.Rebind(`
SELECT packages.ecosystem, artifacts.storage_path, artifacts.content_hash, artifacts.size,
artifacts.content_type, versions.integrity
Expand All @@ -235,14 +238,42 @@ func (db *DB) GetCachedArtifact(packagePURL, versionPURL, filename string) (*Cac
WHERE packages.purl = ? AND artifacts.version_purl = ? AND artifacts.filename = ?
AND artifacts.storage_path IS NOT NULL AND artifacts.fetched_at IS NOT NULL
`)
err := db.Get(&artifact, query, packagePURL, versionPURL, filename)
err := db.Get(&row, query, packagePURL, versionPURL, filename)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
return &artifact, nil
return row.artifact(versionPURL, filename), nil
}

type cachedArtifactRow struct {
Ecosystem string `db:"ecosystem"`
StoragePath string `db:"storage_path"`
ContentHash sql.NullString `db:"content_hash"`
Size sql.NullInt64 `db:"size"`
ContentType sql.NullString `db:"content_type"`
Integrity sql.NullString `db:"integrity"`
}

// artifact converts a cached artifact row to a CachedArtifact without
// validation. A malformed hash or integrity value is handled by
// checkCache, which clears the record and treats the request as a cache
// miss so the client is served a fresh fetch instead of an error.
func (row cachedArtifactRow) artifact(versionPURL, filename string) *CachedArtifact {
return &CachedArtifact{
Ecosystem: row.Ecosystem,
StoragePath: row.StoragePath,
Integrity: row.Integrity,
Artifact: artifacts.Artifact{
PURL: versionPURL,
Digest: digest.Digest("sha256:" + row.ContentHash.String),
Size: row.Size.Int64,
Filename: filename,
MediaType: row.ContentType.String,
},
}
}

func (db *DB) GetArtifactByPath(storagePath string) (*Artifact, error) {
Expand Down
12 changes: 6 additions & 6 deletions internal/database/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"net/url"
"strings"
"time"

"github.com/git-pkgs/artifacts"
)

// Package represents a package in the database.
Expand Down Expand Up @@ -148,12 +150,10 @@ func (a *Artifact) IsCached() bool {

// CachedArtifact contains the fields needed to serve a cached artifact.
type CachedArtifact struct {
Ecosystem string `db:"ecosystem"`
StoragePath string `db:"storage_path"`
ContentHash sql.NullString `db:"content_hash"`
Size sql.NullInt64 `db:"size"`
ContentType sql.NullString `db:"content_type"`
Integrity sql.NullString `db:"integrity"`
Ecosystem string
StoragePath string
Artifact artifacts.Artifact
Integrity sql.NullString
}

// MetadataCacheEntry represents a cached metadata blob for offline serving.
Expand Down
4 changes: 2 additions & 2 deletions internal/handler/apk.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,8 @@ func (h *APKHandler) handlePackageDownload(w http.ResponseWriter, r *http.Reques
return
}

if result.ContentType == "" {
result.ContentType = "application/octet-stream"
if result.Artifact.MediaType == "" {
result.Artifact.MediaType = "application/octet-stream"
}
serveArtifact(w, r.Method, result)
}
Expand Down
8 changes: 4 additions & 4 deletions internal/handler/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,8 @@ func (h *ContainerHandler) handleBlobDownload(w http.ResponseWriter, r *http.Req
}
if cached != nil {
w.Header().Set("Docker-Content-Digest", digest)
if cached.ContentType == "" {
cached.ContentType = "application/octet-stream"
if cached.Artifact.MediaType == "" {
cached.Artifact.MediaType = "application/octet-stream"
}
serveArtifact(w, r.Method, cached)
return
Expand Down Expand Up @@ -205,8 +205,8 @@ func (h *ContainerHandler) handleBlobDownload(w http.ResponseWriter, r *http.Req
}

w.Header().Set("Docker-Content-Digest", digest)
if result.ContentType == "" {
result.ContentType = "application/octet-stream"
if result.Artifact.MediaType == "" {
result.Artifact.MediaType = "application/octet-stream"
}
ServeArtifact(w, result)
}
Expand Down
3 changes: 2 additions & 1 deletion internal/handler/download_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,14 @@ func seedPackageWithPURL(t *testing.T, db *database.DB, store *mockStorage, ecos

storagePath := storage.ArtifactPath(ecosystem, "", name, version, filename)
store.files[storagePath] = []byte(content)
sharedArtifact := testArtifact(content, versionPURL, filename, "application/octet-stream")

art := &database.Artifact{
VersionPURL: versionPURL,
Filename: filename,
UpstreamURL: "https://example.com/" + filename,
StoragePath: sql.NullString{String: storagePath, Valid: true},
ContentHash: sql.NullString{String: sha256Hex(content), Valid: true},
ContentHash: sql.NullString{String: sharedArtifact.Digest.Encoded(), Valid: true},
Size: sql.NullInt64{Int64: int64(len(content)), Valid: true},
ContentType: sql.NullString{String: "application/octet-stream", Valid: true},
FetchedAt: sql.NullTime{Time: time.Now(), Valid: true},
Expand Down
4 changes: 2 additions & 2 deletions internal/handler/generic.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,8 @@ func (h *GenericHandler) handleReleaseAsset(w http.ResponseWriter, r *http.Reque
return
}

if result.ContentType == "" {
result.ContentType = "application/octet-stream"
if result.Artifact.MediaType == "" {
result.Artifact.MediaType = "application/octet-stream"
}
serveArtifact(w, r.Method, result)
}
Expand Down
68 changes: 38 additions & 30 deletions internal/handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"sync"
"time"

"github.com/git-pkgs/artifacts"
"github.com/git-pkgs/cooldown"
"github.com/git-pkgs/proxy/internal/database"
"github.com/git-pkgs/proxy/internal/metrics"
Expand All @@ -24,6 +25,7 @@ import (
"github.com/git-pkgs/proxy/internal/storage"
"github.com/git-pkgs/purl"
"github.com/git-pkgs/registries/fetch"
"github.com/opencontainers/go-digest"
)

// containsPathTraversal returns true if the path contains ".." segments
Expand Down Expand Up @@ -205,9 +207,7 @@ func NewProxy(db *database.DB, store storage.Storage, fetcher fetch.FetcherInter
type CacheResult struct {
Reader io.ReadCloser
RedirectURL string
Size int64
ContentType string
Hash string
Artifact artifacts.Artifact
Cached bool
storagePath string
}
Expand Down Expand Up @@ -270,16 +270,14 @@ func (p *Proxy) checkCache(ctx context.Context, pkgPURL, versionPURL, filename s
if artifact == nil {
return nil, nil
}
checks, err := newIntegrityChecks(artifact.ContentHash.String, artifact.Integrity.String)
checks, err := newIntegrityChecks(artifact.Artifact.Digest.Encoded(), artifact.Integrity.String)
if err != nil {
p.rejectUnusableCacheRecord(artifact, versionPURL, filename, err)
return nil, nil
}

result := &CacheResult{
Size: artifact.Size.Int64,
ContentType: artifact.ContentType.String,
Hash: artifact.ContentHash.String,
Artifact: artifact.Artifact,
Cached: true,
storagePath: artifact.StoragePath,
}
Expand Down Expand Up @@ -439,8 +437,16 @@ func (p *Proxy) storeArtifact(ctx context.Context, ecosystem, name, version, fil
}
}

sharedArtifact := artifacts.Artifact{
PURL: versionPURL,
Digest: digest.Digest("sha256:" + hash),
Size: size,
Filename: filename,
MediaType: artifact.ContentType,
}

// Update database
if err := p.updateCacheDB(ecosystem, name, filename, pkgPURL, versionPURL, upstreamURL, storagePath, hash, size, artifact.ContentType); err != nil {
if err := p.updateCacheDB(ecosystem, name, pkgPURL, upstreamURL, storagePath, sharedArtifact); err != nil {
p.Logger.Warn("failed to update cache database", "error", err)
// Continue anyway - we have the file
}
Expand All @@ -456,11 +462,9 @@ func (p *Proxy) storeArtifact(ctx context.Context, ecosystem, name, version, fil
}

return &CacheResult{
Reader: reader,
Size: size,
ContentType: artifact.ContentType,
Hash: hash,
Cached: false,
Reader: reader,
Artifact: sharedArtifact,
Cached: false,
}, nil
}

Expand Down Expand Up @@ -501,7 +505,7 @@ func (p *Proxy) runScan(ctx context.Context, ecosystem, name, version, filename,
return nil
}

func (p *Proxy) updateCacheDB(ecosystem, name, filename, pkgPURL, versionPURL, upstreamURL, storagePath, hash string, size int64, contentType string) error {
func (p *Proxy) updateCacheDB(ecosystem, name, pkgPURL, upstreamURL, storagePath string, artifact artifacts.Artifact) error {
now := time.Now()

// Upsert package
Expand All @@ -518,7 +522,7 @@ func (p *Proxy) updateCacheDB(ecosystem, name, filename, pkgPURL, versionPURL, u

// Upsert version
ver := &database.Version{
PURL: versionPURL,
PURL: artifact.PURL,
PackagePURL: pkgPURL,
EnrichedAt: sql.NullTime{Time: now, Valid: true},
}
Expand All @@ -528,13 +532,13 @@ func (p *Proxy) updateCacheDB(ecosystem, name, filename, pkgPURL, versionPURL, u

// Upsert artifact
art := &database.Artifact{
VersionPURL: versionPURL,
Filename: filename,
VersionPURL: artifact.PURL,
Filename: artifact.Filename,
UpstreamURL: upstreamURL,
StoragePath: sql.NullString{String: storagePath, Valid: true},
ContentHash: sql.NullString{String: hash, Valid: true},
Size: sql.NullInt64{Int64: size, Valid: true},
ContentType: sql.NullString{String: contentType, Valid: true},
ContentHash: sql.NullString{String: artifact.Digest.Encoded(), Valid: true},
Size: sql.NullInt64{Int64: artifact.Size, Valid: true},
ContentType: sql.NullString{String: artifact.MediaType, Valid: true},
FetchedAt: sql.NullTime{Time: now, Valid: true},
}
if err := p.DB.UpsertArtifact(art); err != nil {
Expand All @@ -550,9 +554,13 @@ func ServeArtifact(w http.ResponseWriter, result *CacheResult) {
}

func serveArtifact(w http.ResponseWriter, method string, result *CacheResult) {
contentHash := ""
if result.Artifact.Digest != "" {
contentHash = result.Artifact.Digest.Encoded()
}
if result.RedirectURL != "" {
if result.Hash != "" {
w.Header().Set(headerETag, `"`+result.Hash+`"`)
if contentHash != "" {
w.Header().Set(headerETag, `"`+contentHash+`"`)
}
w.Header().Set("Location", result.RedirectURL)
w.WriteHeader(http.StatusFound)
Expand All @@ -563,14 +571,14 @@ func serveArtifact(w http.ResponseWriter, method string, result *CacheResult) {
defer func() { _ = result.Reader.Close() }()
}

if result.ContentType != "" {
w.Header().Set(headerContentType, result.ContentType)
if result.Artifact.MediaType != "" {
w.Header().Set(headerContentType, result.Artifact.MediaType)
}
if result.Size > 0 || (method == http.MethodHead && result.Size == 0) {
w.Header().Set(headerContentLength, strconv.FormatInt(result.Size, 10))
if result.Artifact.Size > 0 || (method == http.MethodHead && result.Artifact.Size == 0) {
w.Header().Set(headerContentLength, strconv.FormatInt(result.Artifact.Size, 10))
}
if result.Hash != "" {
w.Header().Set(headerETag, `"`+result.Hash+`"`)
if contentHash != "" {
w.Header().Set(headerETag, `"`+contentHash+`"`)
}

w.WriteHeader(http.StatusOK)
Expand Down Expand Up @@ -1107,15 +1115,15 @@ func (p *Proxy) getCachedArtifactWithUpstreamHash(ctx context.Context, pkgPURL,
if err != nil || cached == nil {
return cached, err
}
if artifactHashMatches(cached.Hash, upstreamHash) {
if artifactHashMatches(cached.Artifact.Digest.Encoded(), upstreamHash) {
return cached, nil
}

if cached.Reader != nil {
_ = cached.Reader.Close()
}
p.Logger.Warn("cached artifact hash disagrees with upstream metadata, discarding",
"purl", versionPURL, "filename", filename, "cached", cached.Hash, "upstream", upstreamHash)
"purl", versionPURL, "filename", filename, "cached", cached.Artifact.Digest.Encoded(), "upstream", upstreamHash)
p.discardCachedArtifact(ctx, versionPURL, filename, cached.storagePath)
return nil, nil
}
Expand Down
Loading