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/artifact_size.go
simon chrzanowski b7b7a64239
Add a size limit for upstream artifacts
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
2026-10-14 08:41:57 +02:00

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
}