mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-09 07:39:19 -05:00
159 lines
4.8 KiB
Go
159 lines
4.8 KiB
Go
package handler
|
|
|
|
import (
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
|
|
"golang.org/x/sync/singleflight"
|
|
)
|
|
|
|
// publishTimeLookup reads a version's publish time from upstream metadata. ok
|
|
// is false when the metadata has no usable time for the version. withholdUnknown
|
|
// marks a listed version missing from stale publish-time metadata.
|
|
type publishTimeLookup func() (published time.Time, ok, withholdUnknown bool, err error)
|
|
|
|
// downloadVersion names the version an artifact download is for, so its
|
|
// cooldown can be checked before the artifact is served.
|
|
type downloadVersion struct {
|
|
ecosystem string
|
|
// policyName is the package name the cooldown policy is keyed by,
|
|
// canonicalPackagePURL(ecosystem, policyName).
|
|
policyName string
|
|
// storedName and version name the versions row the publish time is kept
|
|
// in, the same names the artifact is cached under. An empty storedName
|
|
// keeps no time, and every check asks lookup.
|
|
storedName string
|
|
version string
|
|
cacheName string
|
|
filename string
|
|
lookup publishTimeLookup
|
|
}
|
|
|
|
// withholdInCooldown reports whether a download is inside the cooldown
|
|
// window, or a lookup failure prevents an uncached download.
|
|
func (p *Proxy) withholdInCooldown(w http.ResponseWriter, r *http.Request, d downloadVersion) bool {
|
|
withheld, err := p.versionInCooldown(d)
|
|
if err != nil {
|
|
if !p.serveCachedDespiteCooldownError(w, r, d.ecosystem, d.cacheName, d.version, d.filename, err) {
|
|
p.writeCooldownLookupError(w, d.ecosystem, d.policyName, err)
|
|
}
|
|
return true
|
|
}
|
|
if !withheld {
|
|
return false
|
|
}
|
|
p.Logger.Info("cooldown: withholding "+d.ecosystem+" download",
|
|
"name", d.policyName, "version", d.version)
|
|
http.Error(w, ErrVersionNotFound.Error(), http.StatusNotFound)
|
|
return true
|
|
}
|
|
|
|
func (p *Proxy) versionInCooldown(d downloadVersion) (bool, error) {
|
|
policyKey := canonicalPackagePURL(d.ecosystem, d.policyName)
|
|
if !p.cooldownApplies(d.ecosystem, d.policyName) {
|
|
return false, nil
|
|
}
|
|
|
|
var versionPURL string
|
|
if d.storedName != "" {
|
|
versionPURL = canonicalVersionPURL(d.ecosystem, d.storedName, d.version)
|
|
if ver, err := p.DB.GetVersionByPURL(versionPURL); err == nil && ver != nil && ver.PublishedAt.Valid {
|
|
return !p.cooldown().IsAllowed(d.ecosystem, policyKey, ver.PublishedAt.Time), nil
|
|
}
|
|
}
|
|
|
|
published, ok, withholdUnknown, err := d.lookup()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if !ok {
|
|
return withholdUnknown, nil
|
|
}
|
|
|
|
if versionPURL != "" {
|
|
if err := p.DB.SetVersionPublishedAt(versionPURL, canonicalPackagePURL(d.ecosystem, d.storedName), published); err != nil {
|
|
p.Logger.Warn("cooldown: could not store publish time",
|
|
"ecosystem", d.ecosystem, "name", d.policyName, "version", d.version, "error", err)
|
|
}
|
|
}
|
|
|
|
return !p.cooldown().IsAllowed(d.ecosystem, policyKey, published), nil
|
|
}
|
|
|
|
// maxPublishIndexes bounds how many documents publishIndexCache keeps an
|
|
// index for.
|
|
const maxPublishIndexes = 64
|
|
|
|
// publishIndexCache keeps, for each large metadata document, the publish
|
|
// times a download check reads from it, so a document is decompressed and
|
|
// parsed once per version from upstream rather than on every download.
|
|
// Concurrent checks for a document not indexed yet share one parse.
|
|
type publishIndexCache struct {
|
|
mu sync.Mutex
|
|
entries map[string]publishIndexEntry
|
|
group singleflight.Group
|
|
}
|
|
|
|
type publishIndexEntry struct {
|
|
digest string
|
|
index any
|
|
expires time.Time
|
|
}
|
|
|
|
// publishIndex reuses parsed documents by digest, or for a minute without one.
|
|
func publishIndex[V any](p *Proxy, key string, load func() metadataResult, build func(metadataResult) (V, error)) (V, error) { //nolint:ireturn // Supports timestamp maps and Helm lookups.
|
|
c := &p.sharedProxy().publishIndexes
|
|
fresh := func() (V, bool) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if e, ok := c.entries[key]; ok && !e.expires.IsZero() && p.clock().Before(e.expires) {
|
|
return e.index.(V), true
|
|
}
|
|
var zero V
|
|
return zero, false
|
|
}
|
|
if index, ok := fresh(); ok {
|
|
return index, nil
|
|
}
|
|
v, err, _ := c.group.Do(key, func() (any, error) {
|
|
if index, ok := fresh(); ok {
|
|
return index, nil
|
|
}
|
|
res := load()
|
|
if res.err == nil && res.digest != "" {
|
|
c.mu.Lock()
|
|
entry, ok := c.entries[key]
|
|
c.mu.Unlock()
|
|
if ok && entry.digest == res.digest {
|
|
return entry.index, nil
|
|
}
|
|
}
|
|
index, err := build(res)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if c.entries == nil {
|
|
c.entries = make(map[string]publishIndexEntry)
|
|
}
|
|
if _, ok := c.entries[key]; !ok && len(c.entries) >= maxPublishIndexes {
|
|
for evict := range c.entries {
|
|
delete(c.entries, evict)
|
|
break
|
|
}
|
|
}
|
|
entry := publishIndexEntry{digest: res.digest, index: index}
|
|
if res.digest == "" || !p.CacheMetadata {
|
|
entry.expires = p.clock().Add(time.Minute)
|
|
}
|
|
c.entries[key] = entry
|
|
return index, nil
|
|
})
|
|
if err != nil {
|
|
var zero V
|
|
return zero, err
|
|
}
|
|
return v.(V), nil
|
|
}
|