mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-09 23:59:16 -05:00
170 lines
5.6 KiB
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)
|
|
}
|
|
}
|