2026-05-03 10:36:28 +01:00
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
2026-08-17 09:20:11 +01:00
|
|
|
|
|
|
|
|
"github.com/git-pkgs/integrity"
|
2026-05-03 10:36:28 +01:00
|
|
|
)
|
|
|
|
|
|
2026-08-17 09:20:11 +01:00
|
|
|
type integrityChecks struct {
|
|
|
|
|
contentHash integrity.SRI
|
|
|
|
|
native integrity.SRI
|
2026-10-14 15:43:39 +02:00
|
|
|
// checksum is native metadata given as a single algorithm-prefixed hex
|
|
|
|
|
// checksum, such as a registry's SHA-1 shasum, which is not SRI.
|
|
|
|
|
checksum integrity.Digest
|
|
|
|
|
hasChecksum bool
|
2026-08-17 09:20:11 +01:00
|
|
|
algorithms []integrity.Algorithm
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func newIntegrityChecks(contentHash, native string) (integrityChecks, error) {
|
|
|
|
|
checks := integrityChecks{}
|
|
|
|
|
|
|
|
|
|
if contentHash != "" {
|
|
|
|
|
digest, err := integrity.ParseHex(integrity.SHA256, contentHash)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return integrityChecks{}, fmt.Errorf("parse content_hash: %w", err)
|
|
|
|
|
}
|
|
|
|
|
checks.contentHash = integrity.SRI{digest}
|
|
|
|
|
checks.algorithms = append(checks.algorithms, integrity.SHA256)
|
2026-05-03 10:36:28 +01:00
|
|
|
}
|
2026-08-17 09:20:11 +01:00
|
|
|
|
|
|
|
|
if native != "" {
|
|
|
|
|
digests, err := integrity.ParseSRI(native)
|
|
|
|
|
if err != nil {
|
2026-10-14 15:43:39 +02:00
|
|
|
checksum, checksumErr := integrity.ParseChecksum(native)
|
|
|
|
|
if checksumErr != nil {
|
|
|
|
|
return integrityChecks{}, fmt.Errorf("parse integrity: %w", err)
|
|
|
|
|
}
|
|
|
|
|
checks.checksum, checks.hasChecksum = checksum, true
|
|
|
|
|
checks.algorithms = append(checks.algorithms, checksum.Algorithm())
|
|
|
|
|
return checks, nil
|
2026-08-17 09:20:11 +01:00
|
|
|
}
|
|
|
|
|
checks.native = digests
|
|
|
|
|
for _, digest := range digests {
|
|
|
|
|
checks.algorithms = append(checks.algorithms, digest.Algorithm())
|
|
|
|
|
}
|
2026-05-03 10:36:28 +01:00
|
|
|
}
|
2026-08-17 09:20:11 +01:00
|
|
|
|
|
|
|
|
return checks, nil
|
2026-05-03 10:36:28 +01:00
|
|
|
}
|
|
|
|
|
|
2026-10-14 15:36:26 +02:00
|
|
|
// verifyIntegrity wraps body to check it against the declared integrity as
|
|
|
|
|
// it is read. verified, called once body has been read, reports a mismatch as
|
|
|
|
|
// ErrArtifactDigestMismatch, and also a body that was not read to its end,
|
|
|
|
|
// since only a complete body can be checked. An empty declared checks
|
|
|
|
|
// nothing.
|
|
|
|
|
func verifyIntegrity(body io.ReadCloser, declared string) (_ io.Reader, verified func() error, _ error) {
|
|
|
|
|
if declared == "" {
|
|
|
|
|
return body, func() error { return nil }, nil
|
|
|
|
|
}
|
|
|
|
|
checks, err := newIntegrityChecks("", declared)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, nil, err
|
|
|
|
|
}
|
|
|
|
|
var mismatch string
|
|
|
|
|
reader, err := checks.newVerifyingReader(body, func(reason string) { mismatch = reason }, false)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, nil, err
|
|
|
|
|
}
|
|
|
|
|
return reader, func() error {
|
|
|
|
|
if !reader.(*verifyingReader).verified {
|
|
|
|
|
return fmt.Errorf("%w: body was not read to its end", ErrArtifactDigestMismatch)
|
|
|
|
|
}
|
|
|
|
|
if mismatch != "" {
|
|
|
|
|
return fmt.Errorf("%w: %s", ErrArtifactDigestMismatch, mismatch)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 09:20:11 +01:00
|
|
|
func (c integrityChecks) wrap(source io.ReadCloser, onMismatch func(string)) (io.ReadCloser, error) {
|
Support HTTP byte-range requests for local file cached artifact downloads (#404)
* feat(storage): expose efficient seek for local files
Open file-backed artifacts with os.Open so local readers support
efficient seeking. Leave cloud storage readers unchanged.
Keep the legacy sidecar mapping test focused on fileblob's reader:
Blob.Open now reads local files directly and no longer consults the
sidecar. Continue checking sidecar cleanup and post-store reads.
* feat(handler): preserve efficient seek through integrity checks
Keep the cache result's single Reader field and preserve io.Seeker
when the underlying storage reader supports efficient seeking.
Sequential reads continue through whole-object integrity verification.
A successful seek switches subsequent reads to the source for range
responses, where full-object verification is not possible.
Test both modes and verify the source is closed once.
* feat(handler): add request-aware byte range responses
Add a request-aware artifact serving helper for bounded, open-ended,
and suffix byte ranges, including If-Range handling, 206 responses,
and 416 responses for valid unsatisfiable ranges.
Ignore malformed and multi-range requests by serving the full artifact.
Advertise byte-range support only for seekable readers, preserving HEAD,
redirect, and non-seekable behavior. Clarify that net/http recovers
ErrAbortHandler per request and keeps the server running.
* feat(handler): enable byte ranges across artifact downloads
Forward each artifact request to the shared range-aware response helper,
including cached generic release assets and OCI blobs. This lets the
handler honor ranges only when its reader supports efficient seeking,
without changing direct-storage redirect behavior.
Add warm-cache endpoint tests that assert partial response headers and
bytes without an upstream refetch. Document supported range behavior,
unsupported reader fallback, and the integrity limitation of partial reads.
2026-10-04 09:48:41 -07:00
|
|
|
if len(c.algorithms) == 0 {
|
|
|
|
|
return source, nil
|
|
|
|
|
}
|
|
|
|
|
verified, err := c.newVerifyingReader(source, onMismatch, false)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
seeker, ok := source.(io.Seeker)
|
|
|
|
|
if !ok {
|
|
|
|
|
return verified, nil
|
|
|
|
|
}
|
|
|
|
|
return &seekableVerifyingReader{
|
|
|
|
|
source: source,
|
|
|
|
|
verified: verified,
|
|
|
|
|
seeker: seeker,
|
|
|
|
|
}, nil
|
Add storage.cache_artifacts to serve artifacts without storing them (#387)
* Add storage.passthrough to serve artifacts without storing them
With storage.passthrough enabled the proxy streams every artifact from
upstream to the client and never writes it to storage or the cache
database. Metadata filtering, cooldown and the denylist work as before,
so the proxy can sit behind another cache (e.g. an Artifactory remote)
purely as a policy layer without holding a second copy of every package.
Artifacts whose digest is known up front (OCI, Swift, Helm) are verified
while streaming. Their responses are sent chunked and the connection is
aborted on a mismatch, so a client never receives a tampered artifact as
a complete response.
Passthrough is rejected together with scanning, direct_serve and
mirror_api, and the mirror command refuses to run with it, since all of
them depend on stored artifacts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Address review: abort truncated streams, guard every cache read
- serveArtifact aborts the response on any body read error, and when fewer
bytes than the declared size were written, not only on a digest mismatch.
A failed or short upstream body in passthrough mode no longer reaches the
client as a complete 200.
- Streamed upstream read failures are logged and counted as
stream_failed upstream errors before the response is aborted.
- checkCache reports a miss when artifact caching is off, so every cache
read path, including Swift archive HEAD requests, ignores entries stored
before the mode was enabled. Denylisted versions are still rejected.
- Rename storage.passthrough to storage.cache_artifacts (default true,
PROXY_STORAGE_CACHE_ARTIFACTS), which names what actually changes.
Tests cover an OCI blob shorter than its Content-Length and an npm tarball
whose chunked response ends without the final chunk, both served through
the HTTP handlers, plus a Swift HEAD request against a cached archive.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Rename Proxy.Passthrough to StreamArtifacts
Match the storage.cache_artifacts option: the field says what changes
(artifacts are streamed instead of stored) and its zero value keeps the
usual caching behaviour for every Proxy built without the server config,
such as the mirror command and tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:28:20 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// wrapFailOnMismatch is wrap for bytes that have not been checked anywhere
|
|
|
|
|
// else: a mismatch is also returned from Read as ErrArtifactDigestMismatch in
|
|
|
|
|
// place of io.EOF, so the caller can abort rather than complete the response.
|
|
|
|
|
func (c integrityChecks) wrapFailOnMismatch(source io.ReadCloser, onMismatch func(string)) (io.ReadCloser, error) {
|
|
|
|
|
return c.newVerifyingReader(source, onMismatch, true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c integrityChecks) newVerifyingReader(source io.ReadCloser, onMismatch func(string), failOnMismatch bool) (io.ReadCloser, error) {
|
2026-08-17 09:20:11 +01:00
|
|
|
if len(c.algorithms) == 0 {
|
|
|
|
|
return source, nil
|
|
|
|
|
}
|
|
|
|
|
reader, err := integrity.NewReader(source, c.algorithms...)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("create integrity reader: %w", err)
|
2026-05-03 10:36:28 +01:00
|
|
|
}
|
2026-08-17 09:20:11 +01:00
|
|
|
return &verifyingReader{
|
Add storage.cache_artifacts to serve artifacts without storing them (#387)
* Add storage.passthrough to serve artifacts without storing them
With storage.passthrough enabled the proxy streams every artifact from
upstream to the client and never writes it to storage or the cache
database. Metadata filtering, cooldown and the denylist work as before,
so the proxy can sit behind another cache (e.g. an Artifactory remote)
purely as a policy layer without holding a second copy of every package.
Artifacts whose digest is known up front (OCI, Swift, Helm) are verified
while streaming. Their responses are sent chunked and the connection is
aborted on a mismatch, so a client never receives a tampered artifact as
a complete response.
Passthrough is rejected together with scanning, direct_serve and
mirror_api, and the mirror command refuses to run with it, since all of
them depend on stored artifacts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Address review: abort truncated streams, guard every cache read
- serveArtifact aborts the response on any body read error, and when fewer
bytes than the declared size were written, not only on a digest mismatch.
A failed or short upstream body in passthrough mode no longer reaches the
client as a complete 200.
- Streamed upstream read failures are logged and counted as
stream_failed upstream errors before the response is aborted.
- checkCache reports a miss when artifact caching is off, so every cache
read path, including Swift archive HEAD requests, ignores entries stored
before the mode was enabled. Denylisted versions are still rejected.
- Rename storage.passthrough to storage.cache_artifacts (default true,
PROXY_STORAGE_CACHE_ARTIFACTS), which names what actually changes.
Tests cover an OCI blob shorter than its Content-Length and an npm tarball
whose chunked response ends without the final chunk, both served through
the HTTP handlers, plus a Swift HEAD request against a cached archive.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Rename Proxy.Passthrough to StreamArtifacts
Match the storage.cache_artifacts option: the field says what changes
(artifacts are streamed instead of stored) and its zero value keeps the
usual caching behaviour for every Proxy built without the server config,
such as the mirror command and tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:28:20 +07:00
|
|
|
source: source,
|
|
|
|
|
reader: reader,
|
|
|
|
|
checks: c,
|
|
|
|
|
onMismatch: onMismatch,
|
|
|
|
|
failOnMismatch: failOnMismatch,
|
2026-08-17 09:20:11 +01:00
|
|
|
}, nil
|
2026-05-03 10:36:28 +01:00
|
|
|
}
|
|
|
|
|
|
2026-08-17 09:20:11 +01:00
|
|
|
// verifyingReader forwards Close to its source and reports completed digest
|
|
|
|
|
// mismatches after its shared integrity reader observes EOF.
|
2026-05-03 10:36:28 +01:00
|
|
|
type verifyingReader struct {
|
2026-08-17 09:20:11 +01:00
|
|
|
source io.ReadCloser
|
|
|
|
|
reader *integrity.Reader
|
|
|
|
|
checks integrityChecks
|
2026-05-03 10:36:28 +01:00
|
|
|
onMismatch func(reason string)
|
|
|
|
|
verified bool
|
Add storage.cache_artifacts to serve artifacts without storing them (#387)
* Add storage.passthrough to serve artifacts without storing them
With storage.passthrough enabled the proxy streams every artifact from
upstream to the client and never writes it to storage or the cache
database. Metadata filtering, cooldown and the denylist work as before,
so the proxy can sit behind another cache (e.g. an Artifactory remote)
purely as a policy layer without holding a second copy of every package.
Artifacts whose digest is known up front (OCI, Swift, Helm) are verified
while streaming. Their responses are sent chunked and the connection is
aborted on a mismatch, so a client never receives a tampered artifact as
a complete response.
Passthrough is rejected together with scanning, direct_serve and
mirror_api, and the mirror command refuses to run with it, since all of
them depend on stored artifacts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Address review: abort truncated streams, guard every cache read
- serveArtifact aborts the response on any body read error, and when fewer
bytes than the declared size were written, not only on a digest mismatch.
A failed or short upstream body in passthrough mode no longer reaches the
client as a complete 200.
- Streamed upstream read failures are logged and counted as
stream_failed upstream errors before the response is aborted.
- checkCache reports a miss when artifact caching is off, so every cache
read path, including Swift archive HEAD requests, ignores entries stored
before the mode was enabled. Denylisted versions are still rejected.
- Rename storage.passthrough to storage.cache_artifacts (default true,
PROXY_STORAGE_CACHE_ARTIFACTS), which names what actually changes.
Tests cover an OCI blob shorter than its Content-Length and an npm tarball
whose chunked response ends without the final chunk, both served through
the HTTP handlers, plus a Swift HEAD request against a cached archive.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Rename Proxy.Passthrough to StreamArtifacts
Match the storage.cache_artifacts option: the field says what changes
(artifacts are streamed instead of stored) and its zero value keeps the
usual caching behaviour for every Proxy built without the server config,
such as the mirror command and tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:28:20 +07:00
|
|
|
|
|
|
|
|
failOnMismatch bool
|
|
|
|
|
mismatched bool
|
2026-05-03 10:36:28 +01:00
|
|
|
}
|
|
|
|
|
|
Support HTTP byte-range requests for local file cached artifact downloads (#404)
* feat(storage): expose efficient seek for local files
Open file-backed artifacts with os.Open so local readers support
efficient seeking. Leave cloud storage readers unchanged.
Keep the legacy sidecar mapping test focused on fileblob's reader:
Blob.Open now reads local files directly and no longer consults the
sidecar. Continue checking sidecar cleanup and post-store reads.
* feat(handler): preserve efficient seek through integrity checks
Keep the cache result's single Reader field and preserve io.Seeker
when the underlying storage reader supports efficient seeking.
Sequential reads continue through whole-object integrity verification.
A successful seek switches subsequent reads to the source for range
responses, where full-object verification is not possible.
Test both modes and verify the source is closed once.
* feat(handler): add request-aware byte range responses
Add a request-aware artifact serving helper for bounded, open-ended,
and suffix byte ranges, including If-Range handling, 206 responses,
and 416 responses for valid unsatisfiable ranges.
Ignore malformed and multi-range requests by serving the full artifact.
Advertise byte-range support only for seekable readers, preserving HEAD,
redirect, and non-seekable behavior. Clarify that net/http recovers
ErrAbortHandler per request and keeps the server running.
* feat(handler): enable byte ranges across artifact downloads
Forward each artifact request to the shared range-aware response helper,
including cached generic release assets and OCI blobs. This lets the
handler honor ranges only when its reader supports efficient seeking,
without changing direct-storage redirect behavior.
Add warm-cache endpoint tests that assert partial response headers and
bytes without an upstream refetch. Document supported range behavior,
unsupported reader fallback, and the integrity limitation of partial reads.
2026-10-04 09:48:41 -07:00
|
|
|
// seekableVerifyingReader verifies ordinary reads until a successful seek,
|
|
|
|
|
// after which reads bypass whole-object verification for range responses.
|
|
|
|
|
type seekableVerifyingReader struct {
|
|
|
|
|
source io.ReadCloser
|
|
|
|
|
verified io.ReadCloser
|
|
|
|
|
seeker io.Seeker
|
|
|
|
|
bypass bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *seekableVerifyingReader) Read(p []byte) (int, error) {
|
|
|
|
|
if r.bypass {
|
|
|
|
|
return r.source.Read(p)
|
|
|
|
|
}
|
|
|
|
|
return r.verified.Read(p)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *seekableVerifyingReader) Seek(offset int64, whence int) (int64, error) {
|
|
|
|
|
position, err := r.seeker.Seek(offset, whence)
|
|
|
|
|
if err == nil {
|
|
|
|
|
r.bypass = true
|
|
|
|
|
}
|
|
|
|
|
return position, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *seekableVerifyingReader) Close() error {
|
|
|
|
|
return r.verified.Close()
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 09:20:11 +01:00
|
|
|
func (r *verifyingReader) Read(p []byte) (int, error) {
|
|
|
|
|
n, err := r.reader.Read(p)
|
2026-05-03 10:36:28 +01:00
|
|
|
if err == io.EOF {
|
2026-08-17 09:20:11 +01:00
|
|
|
r.verify()
|
Add storage.cache_artifacts to serve artifacts without storing them (#387)
* Add storage.passthrough to serve artifacts without storing them
With storage.passthrough enabled the proxy streams every artifact from
upstream to the client and never writes it to storage or the cache
database. Metadata filtering, cooldown and the denylist work as before,
so the proxy can sit behind another cache (e.g. an Artifactory remote)
purely as a policy layer without holding a second copy of every package.
Artifacts whose digest is known up front (OCI, Swift, Helm) are verified
while streaming. Their responses are sent chunked and the connection is
aborted on a mismatch, so a client never receives a tampered artifact as
a complete response.
Passthrough is rejected together with scanning, direct_serve and
mirror_api, and the mirror command refuses to run with it, since all of
them depend on stored artifacts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Address review: abort truncated streams, guard every cache read
- serveArtifact aborts the response on any body read error, and when fewer
bytes than the declared size were written, not only on a digest mismatch.
A failed or short upstream body in passthrough mode no longer reaches the
client as a complete 200.
- Streamed upstream read failures are logged and counted as
stream_failed upstream errors before the response is aborted.
- checkCache reports a miss when artifact caching is off, so every cache
read path, including Swift archive HEAD requests, ignores entries stored
before the mode was enabled. Denylisted versions are still rejected.
- Rename storage.passthrough to storage.cache_artifacts (default true,
PROXY_STORAGE_CACHE_ARTIFACTS), which names what actually changes.
Tests cover an OCI blob shorter than its Content-Length and an npm tarball
whose chunked response ends without the final chunk, both served through
the HTTP handlers, plus a Swift HEAD request against a cached archive.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Rename Proxy.Passthrough to StreamArtifacts
Match the storage.cache_artifacts option: the field says what changes
(artifacts are streamed instead of stored) and its zero value keeps the
usual caching behaviour for every Proxy built without the server config,
such as the mirror command and tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:28:20 +07:00
|
|
|
if r.failOnMismatch && r.mismatched {
|
|
|
|
|
return n, ErrArtifactDigestMismatch
|
|
|
|
|
}
|
2026-05-03 10:36:28 +01:00
|
|
|
}
|
|
|
|
|
return n, err
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 09:20:11 +01:00
|
|
|
func (r *verifyingReader) Close() error {
|
|
|
|
|
return r.source.Close()
|
2026-05-03 10:36:28 +01:00
|
|
|
}
|
|
|
|
|
|
2026-08-17 09:20:11 +01:00
|
|
|
func (r *verifyingReader) verify() {
|
|
|
|
|
if r.verified {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
r.verified = true
|
|
|
|
|
result := r.reader.Result()
|
|
|
|
|
if !result.Complete {
|
2026-05-03 10:36:28 +01:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 09:20:11 +01:00
|
|
|
if len(r.checks.contentHash) > 0 {
|
|
|
|
|
if err := result.Verify(r.checks.contentHash); err != nil {
|
Add storage.cache_artifacts to serve artifacts without storing them (#387)
* Add storage.passthrough to serve artifacts without storing them
With storage.passthrough enabled the proxy streams every artifact from
upstream to the client and never writes it to storage or the cache
database. Metadata filtering, cooldown and the denylist work as before,
so the proxy can sit behind another cache (e.g. an Artifactory remote)
purely as a policy layer without holding a second copy of every package.
Artifacts whose digest is known up front (OCI, Swift, Helm) are verified
while streaming. Their responses are sent chunked and the connection is
aborted on a mismatch, so a client never receives a tampered artifact as
a complete response.
Passthrough is rejected together with scanning, direct_serve and
mirror_api, and the mirror command refuses to run with it, since all of
them depend on stored artifacts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Address review: abort truncated streams, guard every cache read
- serveArtifact aborts the response on any body read error, and when fewer
bytes than the declared size were written, not only on a digest mismatch.
A failed or short upstream body in passthrough mode no longer reaches the
client as a complete 200.
- Streamed upstream read failures are logged and counted as
stream_failed upstream errors before the response is aborted.
- checkCache reports a miss when artifact caching is off, so every cache
read path, including Swift archive HEAD requests, ignores entries stored
before the mode was enabled. Denylisted versions are still rejected.
- Rename storage.passthrough to storage.cache_artifacts (default true,
PROXY_STORAGE_CACHE_ARTIFACTS), which names what actually changes.
Tests cover an OCI blob shorter than its Content-Length and an npm tarball
whose chunked response ends without the final chunk, both served through
the HTTP handlers, plus a Swift HEAD request against a cached archive.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Rename Proxy.Passthrough to StreamArtifacts
Match the storage.cache_artifacts option: the field says what changes
(artifacts are streamed instead of stored) and its zero value keeps the
usual caching behaviour for every Proxy built without the server config,
such as the mirror command and tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:28:20 +07:00
|
|
|
r.mismatched = true
|
2026-08-17 09:20:11 +01:00
|
|
|
r.onMismatch("content_hash: " + err.Error())
|
2026-05-03 10:36:28 +01:00
|
|
|
}
|
|
|
|
|
}
|
2026-08-17 09:20:11 +01:00
|
|
|
if len(r.checks.native) > 0 {
|
|
|
|
|
if err := result.Verify(r.checks.native); err != nil {
|
Add storage.cache_artifacts to serve artifacts without storing them (#387)
* Add storage.passthrough to serve artifacts without storing them
With storage.passthrough enabled the proxy streams every artifact from
upstream to the client and never writes it to storage or the cache
database. Metadata filtering, cooldown and the denylist work as before,
so the proxy can sit behind another cache (e.g. an Artifactory remote)
purely as a policy layer without holding a second copy of every package.
Artifacts whose digest is known up front (OCI, Swift, Helm) are verified
while streaming. Their responses are sent chunked and the connection is
aborted on a mismatch, so a client never receives a tampered artifact as
a complete response.
Passthrough is rejected together with scanning, direct_serve and
mirror_api, and the mirror command refuses to run with it, since all of
them depend on stored artifacts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Address review: abort truncated streams, guard every cache read
- serveArtifact aborts the response on any body read error, and when fewer
bytes than the declared size were written, not only on a digest mismatch.
A failed or short upstream body in passthrough mode no longer reaches the
client as a complete 200.
- Streamed upstream read failures are logged and counted as
stream_failed upstream errors before the response is aborted.
- checkCache reports a miss when artifact caching is off, so every cache
read path, including Swift archive HEAD requests, ignores entries stored
before the mode was enabled. Denylisted versions are still rejected.
- Rename storage.passthrough to storage.cache_artifacts (default true,
PROXY_STORAGE_CACHE_ARTIFACTS), which names what actually changes.
Tests cover an OCI blob shorter than its Content-Length and an npm tarball
whose chunked response ends without the final chunk, both served through
the HTTP handlers, plus a Swift HEAD request against a cached archive.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Rename Proxy.Passthrough to StreamArtifacts
Match the storage.cache_artifacts option: the field says what changes
(artifacts are streamed instead of stored) and its zero value keeps the
usual caching behaviour for every Proxy built without the server config,
such as the mirror command and tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:28:20 +07:00
|
|
|
r.mismatched = true
|
2026-08-17 09:20:11 +01:00
|
|
|
r.onMismatch("integrity: " + err.Error())
|
2026-05-03 10:36:28 +01:00
|
|
|
}
|
|
|
|
|
}
|
2026-10-14 15:43:39 +02:00
|
|
|
if r.checks.hasChecksum {
|
|
|
|
|
if err := result.VerifyDigest(r.checks.checksum); err != nil {
|
|
|
|
|
r.mismatched = true
|
|
|
|
|
r.onMismatch("integrity: " + err.Error())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// sha1Checksum returns a registry's hexadecimal SHA-1 shasum as the
|
|
|
|
|
// "sha1-<hex>" checksum newIntegrityChecks accepts, or "" when it is empty or
|
|
|
|
|
// not a SHA-1 digest.
|
|
|
|
|
func sha1Checksum(shasum string) string {
|
|
|
|
|
if shasum == "" {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
digest, err := integrity.ParseHex(integrity.SHA1, shasum)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
return digest.Checksum()
|
2026-05-03 10:36:28 +01:00
|
|
|
}
|