2023-01-24 15:47:27 +01:00
|
|
|
package registry
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2024-05-16 09:26:44 +02:00
|
|
|
"errors"
|
2023-01-24 15:47:27 +01:00
|
|
|
"fmt"
|
2025-02-05 19:53:10 +01:00
|
|
|
"io"
|
2023-01-24 15:47:27 +01:00
|
|
|
"net/http"
|
2025-02-05 19:53:10 +01:00
|
|
|
"net/netip"
|
2023-01-24 15:47:27 +01:00
|
|
|
"net/url"
|
|
|
|
"path"
|
|
|
|
"strconv"
|
2025-02-05 19:53:10 +01:00
|
|
|
"sync"
|
2023-01-24 15:47:27 +01:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/go-logr/logr"
|
|
|
|
|
2025-05-23 13:40:51 +02:00
|
|
|
"github.com/spegel-org/spegel/pkg/httpx"
|
2024-04-01 18:59:26 +02:00
|
|
|
"github.com/spegel-org/spegel/pkg/metrics"
|
|
|
|
"github.com/spegel-org/spegel/pkg/oci"
|
|
|
|
"github.com/spegel-org/spegel/pkg/routing"
|
2023-01-24 15:47:27 +01:00
|
|
|
)
|
|
|
|
|
2023-08-03 12:19:43 +02:00
|
|
|
const (
|
2025-05-23 13:40:51 +02:00
|
|
|
HeaderSpegelMirrored = "X-Spegel-Mirrored"
|
2023-08-03 12:19:43 +02:00
|
|
|
)
|
|
|
|
|
2025-04-19 13:50:22 +02:00
|
|
|
type RegistryConfig struct {
|
|
|
|
Client *http.Client
|
|
|
|
Log logr.Logger
|
|
|
|
Username string
|
|
|
|
Password string
|
|
|
|
ResolveRetries int
|
|
|
|
ResolveLatestTag bool
|
|
|
|
ResolveTimeout time.Duration
|
|
|
|
}
|
|
|
|
|
|
|
|
func (cfg *RegistryConfig) Apply(opts ...RegistryOption) error {
|
|
|
|
for _, opt := range opts {
|
|
|
|
if opt == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if err := opt(cfg); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
2024-03-15 10:21:00 +01:00
|
|
|
}
|
|
|
|
|
2025-04-19 13:50:22 +02:00
|
|
|
type RegistryOption func(cfg *RegistryConfig) error
|
2024-02-05 14:32:56 +01:00
|
|
|
|
2025-04-19 13:50:22 +02:00
|
|
|
func WithResolveRetries(resolveRetries int) RegistryOption {
|
|
|
|
return func(cfg *RegistryConfig) error {
|
|
|
|
cfg.ResolveRetries = resolveRetries
|
|
|
|
return nil
|
2024-02-05 14:32:56 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-04-19 13:50:22 +02:00
|
|
|
func WithResolveLatestTag(resolveLatestTag bool) RegistryOption {
|
|
|
|
return func(cfg *RegistryConfig) error {
|
|
|
|
cfg.ResolveLatestTag = resolveLatestTag
|
|
|
|
return nil
|
2024-02-05 14:32:56 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-04-19 13:50:22 +02:00
|
|
|
func WithResolveTimeout(resolveTimeout time.Duration) RegistryOption {
|
|
|
|
return func(cfg *RegistryConfig) error {
|
|
|
|
cfg.ResolveTimeout = resolveTimeout
|
|
|
|
return nil
|
2024-02-05 14:32:56 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-04-19 13:50:22 +02:00
|
|
|
func WithTransport(transport http.RoundTripper) RegistryOption {
|
|
|
|
return func(cfg *RegistryConfig) error {
|
|
|
|
if cfg.Client == nil {
|
|
|
|
cfg.Client = &http.Client{}
|
2025-02-05 19:53:10 +01:00
|
|
|
}
|
2025-04-19 13:50:22 +02:00
|
|
|
cfg.Client.Transport = transport
|
|
|
|
return nil
|
2024-02-05 14:32:56 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-04-19 13:50:22 +02:00
|
|
|
func WithLogger(log logr.Logger) RegistryOption {
|
|
|
|
return func(cfg *RegistryConfig) error {
|
|
|
|
cfg.Log = log
|
|
|
|
return nil
|
2024-03-15 10:21:00 +01:00
|
|
|
}
|
2023-06-28 21:52:39 +02:00
|
|
|
}
|
|
|
|
|
2025-04-19 13:50:22 +02:00
|
|
|
func WithBasicAuth(username, password string) RegistryOption {
|
|
|
|
return func(cfg *RegistryConfig) error {
|
|
|
|
cfg.Username = username
|
|
|
|
cfg.Password = password
|
|
|
|
return nil
|
2025-03-05 12:37:46 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-04-19 13:50:22 +02:00
|
|
|
type Registry struct {
|
|
|
|
client *http.Client
|
|
|
|
bufferPool *sync.Pool
|
|
|
|
log logr.Logger
|
2025-05-08 11:46:56 +02:00
|
|
|
ociStore oci.Store
|
2025-04-19 13:50:22 +02:00
|
|
|
router routing.Router
|
|
|
|
username string
|
|
|
|
password string
|
|
|
|
resolveRetries int
|
|
|
|
resolveTimeout time.Duration
|
|
|
|
resolveLatestTag bool
|
|
|
|
}
|
|
|
|
|
2025-05-08 11:46:56 +02:00
|
|
|
func NewRegistry(ociStore oci.Store, router routing.Router, opts ...RegistryOption) (*Registry, error) {
|
2025-04-19 13:50:22 +02:00
|
|
|
transport, ok := http.DefaultTransport.(*http.Transport)
|
|
|
|
if !ok {
|
|
|
|
return nil, errors.New("default transporn is not of type http.Transport")
|
|
|
|
}
|
|
|
|
cfg := RegistryConfig{
|
|
|
|
Client: &http.Client{
|
|
|
|
Transport: transport.Clone(),
|
|
|
|
},
|
|
|
|
Log: logr.Discard(),
|
|
|
|
ResolveRetries: 3,
|
|
|
|
ResolveLatestTag: true,
|
|
|
|
ResolveTimeout: 20 * time.Millisecond,
|
|
|
|
}
|
|
|
|
err := cfg.Apply(opts...)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2025-02-05 19:53:10 +01:00
|
|
|
bufferPool := &sync.Pool{
|
2025-04-15 11:18:41 +02:00
|
|
|
New: func() any {
|
2025-02-05 19:53:10 +01:00
|
|
|
buf := make([]byte, 32*1024)
|
|
|
|
return &buf
|
|
|
|
},
|
|
|
|
}
|
2024-02-05 14:32:56 +01:00
|
|
|
r := &Registry{
|
2025-05-08 11:46:56 +02:00
|
|
|
ociStore: ociStore,
|
2023-07-29 12:06:23 +02:00
|
|
|
router: router,
|
2025-04-19 13:50:22 +02:00
|
|
|
client: cfg.Client,
|
|
|
|
log: cfg.Log,
|
|
|
|
resolveRetries: cfg.ResolveRetries,
|
|
|
|
resolveLatestTag: cfg.ResolveLatestTag,
|
|
|
|
resolveTimeout: cfg.ResolveTimeout,
|
|
|
|
username: cfg.Username,
|
|
|
|
password: cfg.Password,
|
2025-02-05 19:53:10 +01:00
|
|
|
bufferPool: bufferPool,
|
2024-02-05 14:32:56 +01:00
|
|
|
}
|
2025-04-19 13:50:22 +02:00
|
|
|
return r, nil
|
2023-01-24 15:47:27 +01:00
|
|
|
}
|
|
|
|
|
2024-06-19 16:37:43 +02:00
|
|
|
func (r *Registry) Server(addr string) (*http.Server, error) {
|
2025-05-23 13:40:51 +02:00
|
|
|
m := httpx.NewServeMux(r.log)
|
2025-04-28 21:33:39 +02:00
|
|
|
m.Handle("GET /healthz", r.readyHandler)
|
|
|
|
m.Handle("GET /v2/", r.registryHandler)
|
|
|
|
m.Handle("HEAD /v2/", r.registryHandler)
|
2023-01-24 15:47:27 +01:00
|
|
|
srv := &http.Server{
|
|
|
|
Addr: addr,
|
2024-06-19 16:37:43 +02:00
|
|
|
Handler: m,
|
2023-01-24 15:47:27 +01:00
|
|
|
}
|
2024-06-19 16:37:43 +02:00
|
|
|
return srv, nil
|
2023-01-24 15:47:27 +01:00
|
|
|
}
|
|
|
|
|
2025-05-23 13:40:51 +02:00
|
|
|
func (r *Registry) readyHandler(rw httpx.ResponseWriter, req *http.Request) {
|
2025-04-28 21:33:39 +02:00
|
|
|
rw.SetHandler("ready")
|
2024-05-21 00:17:10 +02:00
|
|
|
ok, err := r.router.Ready(req.Context())
|
2023-07-04 12:52:39 +02:00
|
|
|
if err != nil {
|
2024-05-16 09:26:44 +02:00
|
|
|
rw.WriteError(http.StatusInternalServerError, fmt.Errorf("could not determine router readiness: %w", err))
|
2023-07-04 12:52:39 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
if !ok {
|
2024-03-15 10:21:00 +01:00
|
|
|
rw.WriteHeader(http.StatusInternalServerError)
|
2023-07-04 12:52:39 +02:00
|
|
|
return
|
|
|
|
}
|
2023-01-24 22:52:13 +01:00
|
|
|
}
|
|
|
|
|
2025-05-23 13:40:51 +02:00
|
|
|
func (r *Registry) registryHandler(rw httpx.ResponseWriter, req *http.Request) {
|
2025-04-28 21:33:39 +02:00
|
|
|
rw.SetHandler("registry")
|
|
|
|
|
2025-03-05 12:37:46 +01:00
|
|
|
// Check basic authentication
|
|
|
|
if r.username != "" || r.password != "" {
|
|
|
|
username, password, _ := req.BasicAuth()
|
|
|
|
if r.username != username || r.password != password {
|
|
|
|
rw.WriteError(http.StatusUnauthorized, errors.New("invalid basic authentication"))
|
2025-04-28 21:33:39 +02:00
|
|
|
return
|
2025-03-05 12:37:46 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-15 10:21:00 +01:00
|
|
|
// Quickly return 200 for /v2 to indicate that registry supports v2.
|
|
|
|
if path.Clean(req.URL.Path) == "/v2" {
|
2025-04-28 21:33:39 +02:00
|
|
|
rw.SetHandler("v2")
|
2024-05-16 09:26:44 +02:00
|
|
|
rw.WriteHeader(http.StatusOK)
|
2025-04-28 21:33:39 +02:00
|
|
|
return
|
2023-01-24 15:47:27 +01:00
|
|
|
}
|
|
|
|
|
2023-07-03 17:53:36 +02:00
|
|
|
// Parse out path components from request.
|
2025-03-16 17:47:15 +01:00
|
|
|
dist, err := oci.ParseDistributionPath(req.URL)
|
2023-01-24 15:47:27 +01:00
|
|
|
if err != nil {
|
2024-05-16 09:26:44 +02:00
|
|
|
rw.WriteError(http.StatusNotFound, fmt.Errorf("could not parse path according to OCI distribution spec: %w", err))
|
2025-04-28 21:33:39 +02:00
|
|
|
return
|
2023-01-24 15:47:27 +01:00
|
|
|
}
|
2023-05-16 09:10:41 +02:00
|
|
|
|
|
|
|
// Request with mirror header are proxied.
|
2025-05-23 13:40:51 +02:00
|
|
|
if req.Header.Get(HeaderSpegelMirrored) != "true" {
|
2023-08-03 12:19:43 +02:00
|
|
|
// Set mirrored header in request to stop infinite loops
|
2025-05-23 13:40:51 +02:00
|
|
|
req.Header.Set(HeaderSpegelMirrored, "true")
|
2025-04-15 19:33:34 +02:00
|
|
|
|
|
|
|
// If content is present locally we should skip the mirroring and just serve it.
|
|
|
|
var ociErr error
|
|
|
|
if dist.Digest == "" {
|
2025-05-08 11:46:56 +02:00
|
|
|
_, ociErr = r.ociStore.Resolve(req.Context(), dist.Reference())
|
2025-04-15 19:33:34 +02:00
|
|
|
} else {
|
2025-05-08 11:46:56 +02:00
|
|
|
_, ociErr = r.ociStore.Size(req.Context(), dist.Digest)
|
2025-04-15 19:33:34 +02:00
|
|
|
}
|
|
|
|
if ociErr != nil {
|
2025-04-28 21:33:39 +02:00
|
|
|
rw.SetHandler("mirror")
|
2025-04-15 19:33:34 +02:00
|
|
|
r.handleMirror(rw, req, dist)
|
2025-04-28 21:33:39 +02:00
|
|
|
return
|
2025-04-15 19:33:34 +02:00
|
|
|
}
|
2023-01-24 15:47:27 +01:00
|
|
|
}
|
2023-05-16 09:10:41 +02:00
|
|
|
|
|
|
|
// Serve registry endpoints.
|
2025-03-16 17:47:15 +01:00
|
|
|
switch dist.Kind {
|
|
|
|
case oci.DistributionKindManifest:
|
2025-04-28 21:33:39 +02:00
|
|
|
rw.SetHandler("manifest")
|
2025-03-16 17:47:15 +01:00
|
|
|
r.handleManifest(rw, req, dist)
|
2025-04-28 21:33:39 +02:00
|
|
|
return
|
2025-03-16 17:47:15 +01:00
|
|
|
case oci.DistributionKindBlob:
|
2025-04-28 21:33:39 +02:00
|
|
|
rw.SetHandler("blob")
|
2025-03-16 17:47:15 +01:00
|
|
|
r.handleBlob(rw, req, dist)
|
2025-04-28 21:33:39 +02:00
|
|
|
return
|
2024-05-16 09:26:44 +02:00
|
|
|
default:
|
2025-03-16 17:47:15 +01:00
|
|
|
rw.WriteError(http.StatusNotFound, fmt.Errorf("unknown distribution path kind %s", dist.Kind))
|
2025-04-28 21:33:39 +02:00
|
|
|
return
|
2023-01-24 15:47:27 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-05-23 13:40:51 +02:00
|
|
|
func (r *Registry) handleMirror(rw httpx.ResponseWriter, req *http.Request, dist oci.DistributionPath) {
|
2025-04-28 21:33:39 +02:00
|
|
|
log := r.log.WithValues("ref", dist.Reference(), "path", req.URL.Path)
|
2023-06-28 21:52:39 +02:00
|
|
|
|
2024-05-15 21:49:37 +02:00
|
|
|
defer func() {
|
|
|
|
cacheType := "hit"
|
|
|
|
if rw.Status() != http.StatusOK {
|
|
|
|
cacheType = "miss"
|
|
|
|
}
|
2025-04-15 19:33:34 +02:00
|
|
|
metrics.MirrorRequestsTotal.WithLabelValues(dist.Registry, cacheType).Inc()
|
2024-05-15 21:49:37 +02:00
|
|
|
}()
|
|
|
|
|
2025-03-16 17:47:15 +01:00
|
|
|
if !r.resolveLatestTag && dist.IsLatestTag() {
|
|
|
|
r.log.V(4).Info("skipping mirror request for image with latest tag", "image", dist.Reference())
|
2024-05-15 21:49:37 +02:00
|
|
|
rw.WriteHeader(http.StatusNotFound)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2025-03-16 17:47:15 +01:00
|
|
|
// Resolve mirror with the requested reference
|
2024-05-15 21:49:37 +02:00
|
|
|
resolveCtx, cancel := context.WithTimeout(req.Context(), r.resolveTimeout)
|
|
|
|
defer cancel()
|
|
|
|
resolveCtx = logr.NewContext(resolveCtx, log)
|
2025-04-15 19:33:34 +02:00
|
|
|
peerCh, err := r.router.Resolve(resolveCtx, dist.Reference(), r.resolveRetries)
|
2023-01-24 15:47:27 +01:00
|
|
|
if err != nil {
|
2024-05-16 09:26:44 +02:00
|
|
|
rw.WriteError(http.StatusInternalServerError, fmt.Errorf("error occurred when attempting to resolve mirrors: %w", err))
|
2023-10-18 16:22:41 +02:00
|
|
|
return
|
2023-01-24 15:47:27 +01:00
|
|
|
}
|
2024-05-16 09:26:44 +02:00
|
|
|
|
|
|
|
mirrorAttempts := 0
|
2023-06-27 17:53:47 +02:00
|
|
|
for {
|
|
|
|
select {
|
2024-05-16 23:26:31 +02:00
|
|
|
case <-req.Context().Done():
|
2023-10-10 11:03:23 +02:00
|
|
|
// Request has been closed by server or client. No use continuing.
|
2025-03-16 17:47:15 +01:00
|
|
|
rw.WriteError(http.StatusNotFound, fmt.Errorf("mirroring for image component %s has been cancelled: %w", dist.Reference(), resolveCtx.Err()))
|
2023-06-27 17:53:47 +02:00
|
|
|
return
|
2025-04-15 12:05:11 +02:00
|
|
|
case peer, ok := <-peerCh:
|
2023-06-27 17:53:47 +02:00
|
|
|
// Channel closed means no more mirrors will be received and max retries has been reached.
|
|
|
|
if !ok {
|
2025-03-16 17:47:15 +01:00
|
|
|
err = fmt.Errorf("mirror with image component %s could not be found", dist.Reference())
|
2024-05-16 09:26:44 +02:00
|
|
|
if mirrorAttempts > 0 {
|
|
|
|
err = errors.Join(err, fmt.Errorf("requests to %d mirrors failed, all attempts have been exhausted or timeout has been reached", mirrorAttempts))
|
|
|
|
}
|
|
|
|
rw.WriteError(http.StatusNotFound, err)
|
2023-06-27 17:53:47 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-05-16 09:26:44 +02:00
|
|
|
mirrorAttempts++
|
|
|
|
|
2025-04-15 12:05:11 +02:00
|
|
|
err := forwardRequest(r.client, r.bufferPool, req, rw, peer)
|
2025-02-05 19:53:10 +01:00
|
|
|
if err != nil {
|
2025-04-15 12:05:11 +02:00
|
|
|
log.Error(err, "request to mirror failed", "attempt", mirrorAttempts, "path", req.URL.Path, "mirror", peer)
|
2025-02-05 19:53:10 +01:00
|
|
|
continue
|
2023-06-27 17:53:47 +02:00
|
|
|
}
|
2025-04-15 12:05:11 +02:00
|
|
|
log.V(4).Info("mirrored request", "path", req.URL.Path, "mirror", peer)
|
2023-06-27 17:53:47 +02:00
|
|
|
return
|
|
|
|
}
|
2023-01-24 15:47:27 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-05-23 13:40:51 +02:00
|
|
|
func (r *Registry) handleManifest(rw httpx.ResponseWriter, req *http.Request, dist oci.DistributionPath) {
|
2025-03-16 17:47:15 +01:00
|
|
|
if dist.Digest == "" {
|
2025-05-08 11:46:56 +02:00
|
|
|
dgst, err := r.ociStore.Resolve(req.Context(), dist.Reference())
|
2024-05-13 22:32:23 +02:00
|
|
|
if err != nil {
|
2025-03-16 17:47:15 +01:00
|
|
|
rw.WriteError(http.StatusNotFound, fmt.Errorf("could not get digest for image %s: %w", dist.Reference(), err))
|
2024-05-13 22:32:23 +02:00
|
|
|
return
|
|
|
|
}
|
2025-03-16 17:47:15 +01:00
|
|
|
dist.Digest = dgst
|
2024-05-13 22:32:23 +02:00
|
|
|
}
|
2025-05-08 11:46:56 +02:00
|
|
|
b, mediaType, err := r.ociStore.GetManifest(req.Context(), dist.Digest)
|
2023-01-24 15:47:27 +01:00
|
|
|
if err != nil {
|
2025-03-16 17:47:15 +01:00
|
|
|
rw.WriteError(http.StatusNotFound, fmt.Errorf("could not get manifest content for digest %s: %w", dist.Digest.String(), err))
|
2023-01-24 15:47:27 +01:00
|
|
|
return
|
|
|
|
}
|
2025-05-23 13:40:51 +02:00
|
|
|
rw.Header().Set(httpx.HeaderContentType, mediaType)
|
|
|
|
rw.Header().Set(httpx.HeaderContentLength, strconv.FormatInt(int64(len(b)), 10))
|
|
|
|
rw.Header().Set(oci.HeaderDockerDigest, dist.Digest.String())
|
2024-03-15 10:21:00 +01:00
|
|
|
if req.Method == http.MethodHead {
|
2023-01-24 15:47:27 +01:00
|
|
|
return
|
|
|
|
}
|
2024-03-15 10:21:00 +01:00
|
|
|
_, err = rw.Write(b)
|
2023-01-24 15:47:27 +01:00
|
|
|
if err != nil {
|
2024-05-16 09:26:44 +02:00
|
|
|
r.log.Error(err, "error occurred when writing manifest")
|
2023-01-24 15:47:27 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-05-23 13:40:51 +02:00
|
|
|
func (r *Registry) handleBlob(rw httpx.ResponseWriter, req *http.Request, dist oci.DistributionPath) {
|
2025-05-08 11:46:56 +02:00
|
|
|
size, err := r.ociStore.Size(req.Context(), dist.Digest)
|
2023-01-24 15:47:27 +01:00
|
|
|
if err != nil {
|
2025-03-16 17:47:15 +01:00
|
|
|
rw.WriteError(http.StatusInternalServerError, fmt.Errorf("could not determine size of blob with digest %s: %w", dist.Digest.String(), err))
|
2023-01-24 15:47:27 +01:00
|
|
|
return
|
|
|
|
}
|
2025-05-23 13:40:51 +02:00
|
|
|
rw.Header().Set(httpx.HeaderAcceptRanges, "bytes")
|
|
|
|
rw.Header().Set(httpx.HeaderContentType, "application/octet-stream")
|
|
|
|
rw.Header().Set(httpx.HeaderContentLength, strconv.FormatInt(size, 10))
|
|
|
|
rw.Header().Set(oci.HeaderDockerDigest, dist.Digest.String())
|
2024-03-15 10:21:00 +01:00
|
|
|
if req.Method == http.MethodHead {
|
2023-01-24 16:40:48 +01:00
|
|
|
return
|
|
|
|
}
|
2024-10-31 17:35:34 +08:00
|
|
|
|
2025-05-08 11:46:56 +02:00
|
|
|
rc, err := r.ociStore.GetBlob(req.Context(), dist.Digest)
|
2024-03-24 14:42:33 +01:00
|
|
|
if err != nil {
|
2025-03-16 17:47:15 +01:00
|
|
|
rw.WriteError(http.StatusInternalServerError, fmt.Errorf("could not get reader for blob with digest %s: %w", dist.Digest.String(), err))
|
2024-03-24 14:42:33 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
defer rc.Close()
|
2024-09-02 17:54:22 +08:00
|
|
|
|
2024-10-31 17:35:34 +08:00
|
|
|
http.ServeContent(rw, req, "", time.Time{}, rc)
|
2023-01-24 15:47:27 +01:00
|
|
|
}
|
2023-03-10 15:03:15 +01:00
|
|
|
|
2025-02-05 19:53:10 +01:00
|
|
|
func forwardRequest(client *http.Client, bufferPool *sync.Pool, req *http.Request, rw http.ResponseWriter, addrPort netip.AddrPort) error {
|
|
|
|
// Do request to mirror.
|
|
|
|
forwardScheme := "http"
|
|
|
|
if req.TLS != nil {
|
|
|
|
forwardScheme = "https"
|
|
|
|
}
|
|
|
|
u := &url.URL{
|
|
|
|
Scheme: forwardScheme,
|
|
|
|
Host: addrPort.String(),
|
|
|
|
Path: req.URL.Path,
|
|
|
|
RawQuery: req.URL.RawQuery,
|
|
|
|
}
|
|
|
|
forwardReq, err := http.NewRequestWithContext(req.Context(), req.Method, u.String(), nil)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2025-06-11 10:55:59 +02:00
|
|
|
httpx.CopyHeader(forwardReq.Header, req.Header)
|
2025-02-05 19:53:10 +01:00
|
|
|
forwardResp, err := client.Do(forwardReq)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2025-06-05 15:13:03 +02:00
|
|
|
defer httpx.DrainAndClose(forwardResp.Body)
|
|
|
|
err = httpx.CheckResponseStatus(forwardResp, http.StatusOK, http.StatusPartialContent)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2025-02-05 19:53:10 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// TODO (phillebaba): Is it possible to retry if copy fails half way through?
|
|
|
|
// Copy forward response to response writer.
|
2025-06-11 10:55:59 +02:00
|
|
|
httpx.CopyHeader(rw.Header(), forwardResp.Header)
|
2025-02-05 19:53:10 +01:00
|
|
|
rw.WriteHeader(http.StatusOK)
|
|
|
|
//nolint: errcheck // Ignore
|
|
|
|
buf := bufferPool.Get().(*[]byte)
|
|
|
|
defer bufferPool.Put(buf)
|
|
|
|
_, err = io.CopyBuffer(rw, forwardResp.Body, *buf)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|