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/cooldown_times.go

170 lines
5.6 KiB
Go

package handler
import (
"bytes"
"compress/gzip"
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"slices"
"strings"
"time"
)
// publishTimes maps each version a times document lists to its publish
// time. A version whose time didn't parse maps to the zero time and is let
// through.
type publishTimes map[string]time.Time
func (p *Proxy) lookupPublishTime(ctx context.Context, t timedMetadata, version string, listed func(metadataResult) ([]string, error)) (time.Time, bool, bool, error) {
var versions []string
docs, err := p.fetchTimedMetadata(ctx, t, func(doc metadataResult) ([]string, error) {
var err error
versions, err = listed(doc)
return versions, err
})
switch {
case docs.doc.err != nil:
return time.Time{}, false, false, docs.doc.err
case docs.times.err != nil:
return time.Time{}, false, false, docs.times.err
case err != nil:
return time.Time{}, false, false, err
}
published, ok := docs.published[version]
withholdUnknown := !ok && docs.withholdUnknown && slices.Contains(versions, version)
return published, ok && !published.IsZero(), withholdUnknown, nil
}
// timedMetadata describes metadata whose publish times live in a second
// document, as with the RubyGems compact index and the versions API, or a
// Hex package and the Hex HTTP API. Both are read through the metadata cache.
type timedMetadata struct {
ecosystem string
// docKey and docURL are the cache key and upstream URL of the document
// clients read. They match the plain metadata path's, so both share one
// cached copy.
docKey, docURL string
// timesKey and timesURL are those of the document with publish times.
timesKey, timesURL string
timesAccept string
parseTimes func([]byte) (publishTimes, error)
}
// timedDocuments is what fetchTimedMetadata hands back. published is read
// from times and is nil when times.err is set.
type timedDocuments struct {
doc metadataResult
times metadataResult
published publishTimes
// withholdUnknown is set when doc lists versions the times document
// doesn't, and that document is a stale copy: those versions are newer
// than the copy, so they are left out rather than let through.
withholdUnknown bool
}
// fetchTimedMetadata fetches both documents concurrently. The document
// clients read is fetched as the plain metadata path fetches it, with
// identity encoding and Accept */*.
//
// Each document has its own TTL, so a cached times document can be older
// than the document it is used to filter, and miss versions published in
// between. Those versions would be let through as having no publish time.
// listed returns the versions the document lists; when some are missing
// from a times document read from the cache, the times document is fetched
// again before filtering.
func (p *Proxy) fetchTimedMetadata(ctx context.Context, t timedMetadata, listed func(metadataResult) ([]string, error)) (timedDocuments, error) {
var res timedDocuments
var published publishTimes
validate := func(body []byte) error {
parsed, err := t.parseTimes(body)
if err == nil {
published = parsed
}
return err
}
done := make(chan struct{})
go func() {
defer close(done)
res.times = p.lookupMetadata(ctx, t.ecosystem, t.timesKey, t.timesURL, t.timesAccept, "", validate)
}()
res.doc = p.lookupMetadata(ctx, t.ecosystem, t.docKey, t.docURL, "*/*", "identity", nil)
<-done
if res.doc.err != nil || res.times.err != nil {
return res, nil
}
versions, err := listed(res.doc)
if err != nil {
return res, err
}
unknown := func() bool {
for _, v := range versions {
if _, ok := published[v]; !ok {
return true
}
}
return false
}
if unknown() && res.times.fromCache {
p.Logger.Info("cooldown: publish times older than metadata, fetching them again",
"ecosystem", t.ecosystem, "key", t.timesKey)
res.times = p.refreshMetadata(ctx, t.ecosystem, t.timesKey, t.timesURL, t.timesAccept, "", validate)
if res.times.err != nil {
return res, nil
}
}
res.published = published
res.withholdUnknown = res.times.stale && unknown()
return res, nil
}
// rewriteDigest identifies the inputs of a cooldown rewrite for the rewrite
// cache: both documents and whether unknown versions are withheld.
func (d timedDocuments) rewriteDigest() string {
h := sha256.New()
for _, res := range []metadataResult{d.doc, d.times} {
digest := res.digest
if digest == "" {
sum := sha256.Sum256(res.body)
digest = "sha256:" + hex.EncodeToString(sum[:])
}
_, _ = fmt.Fprintf(h, "%d:%s", len(digest), digest)
}
_, _ = fmt.Fprintf(h, "%t", d.withholdUnknown)
return "sha256:" + hex.EncodeToString(h.Sum(nil))
}
// withheld reports whether cooldown leaves version out, given the
// package's publish times.
func (d timedDocuments) withheld(p *Proxy, ecosystem, packagePURL, version string) bool {
published, ok := d.published[version]
if !ok {
return d.withholdUnknown
}
if published.IsZero() {
return false
}
return !p.cooldown().IsAllowed(ecosystem, packagePURL, published)
}
// decodedMetadataBody returns res's body without its Content-Encoding. The
// documents are fetched with identity encoding, so an upstream that sends
// gzip anyway is the only case to handle.
func (p *Proxy) decodedMetadataBody(res metadataResult) ([]byte, error) {
switch strings.ToLower(strings.TrimSpace(res.contentEncoding)) {
case "", "identity":
return res.body, nil
case "gzip", "x-gzip":
zr, err := gzip.NewReader(bytes.NewReader(res.body))
if err != nil {
return nil, err
}
defer func() { _ = zr.Close() }()
return p.ReadMetadata(zr)
default:
return nil, fmt.Errorf("unsupported content encoding %q", res.contentEncoding)
}
}