mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-09 15:49:16 -05:00
Artifacts had no size limit. A fetch was bounded only by the server's write timeout, and storage.max_size evicts only after the bytes are written, so any client could fill storage or keep fetches busy by requesting huge artifacts. - Add artifact_max_size (PROXY_ARTIFACT_MAX_SIZE), unlimited by default - Refuse an artifact whose declared size is over the limit before reading it, and fail one of unknown size once the download passes the limit - Apply it in the fetcher so stored, streamed and mirrored artifacts all honour it; nothing is cached and clients get 502
76 lines
2.3 KiB
Go
76 lines
2.3 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
|
|
"github.com/git-pkgs/registries/fetch"
|
|
)
|
|
|
|
// ErrArtifactTooLarge is returned for an upstream artifact larger than the
|
|
// configured artifact size limit.
|
|
var ErrArtifactTooLarge = errors.New("artifact exceeds size limit")
|
|
|
|
// LimitArtifactSize wraps fetcher so no artifact over maxSize bytes is
|
|
// fetched. An artifact whose declared size is over the limit is refused
|
|
// before its body is read; one of unknown size fails once reading passes the
|
|
// limit, so the store or stream consuming it fails and nothing is cached.
|
|
// A maxSize of zero or less returns fetcher unchanged.
|
|
func LimitArtifactSize(fetcher fetch.FetcherInterface, maxSize int64) fetch.FetcherInterface { //nolint:ireturn // returns fetcher itself when there is no limit
|
|
if maxSize <= 0 {
|
|
return fetcher
|
|
}
|
|
return &sizeLimitedFetcher{FetcherInterface: fetcher, maxSize: maxSize}
|
|
}
|
|
|
|
type sizeLimitedFetcher struct {
|
|
fetch.FetcherInterface
|
|
maxSize int64
|
|
}
|
|
|
|
func (f *sizeLimitedFetcher) Fetch(ctx context.Context, url string) (*fetch.Artifact, error) {
|
|
return f.limit(f.FetcherInterface.Fetch(ctx, url))
|
|
}
|
|
|
|
func (f *sizeLimitedFetcher) FetchWithHeaders(ctx context.Context, url string, headers http.Header) (*fetch.Artifact, error) {
|
|
return f.limit(f.FetcherInterface.FetchWithHeaders(ctx, url, headers))
|
|
}
|
|
|
|
func (f *sizeLimitedFetcher) limit(artifact *fetch.Artifact, err error) (*fetch.Artifact, error) {
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if artifact.Size > f.maxSize {
|
|
_ = artifact.Body.Close()
|
|
return nil, fmt.Errorf("%w: upstream declared %d bytes, limit is %d", ErrArtifactTooLarge, artifact.Size, f.maxSize)
|
|
}
|
|
limited := *artifact
|
|
limited.Body = &sizeLimitedBody{ReadCloser: artifact.Body, remaining: f.maxSize}
|
|
return &limited, nil
|
|
}
|
|
|
|
// sizeLimitedBody fails with ErrArtifactTooLarge once more than the limit has
|
|
// been read. It reads one byte past the limit to tell an artifact of exactly
|
|
// the limit from a larger one.
|
|
type sizeLimitedBody struct {
|
|
io.ReadCloser
|
|
remaining int64
|
|
}
|
|
|
|
func (b *sizeLimitedBody) Read(p []byte) (int, error) {
|
|
if b.remaining < 0 {
|
|
return 0, ErrArtifactTooLarge
|
|
}
|
|
if int64(len(p)) > b.remaining+1 {
|
|
p = p[:b.remaining+1]
|
|
}
|
|
n, err := b.ReadCloser.Read(p)
|
|
b.remaining -= int64(n)
|
|
if b.remaining < 0 {
|
|
return n + int(b.remaining), ErrArtifactTooLarge
|
|
}
|
|
return n, err
|
|
}
|