storage/driver: replace URLFor method
Several storage drivers and storage middlewares need to introspect the client HTTP request in order to construct content-redirect URLs. The request is indirectly passed into the driver interface method URLFor() through the context argument, which is bad practice. The request should be passed in as an explicit argument as the method is only called from request handlers. Replace the URLFor() method with a RedirectURL() method which takes an HTTP request as a parameter instead of a context. Drop the options argument from URLFor() as in practice it only ever encoded the request method, which can now be fetched directly from the request. No URLFor() callers ever passed in an "expiry" option, either. Signed-off-by: Cory Snider <csnider@mirantis.com>
This commit is contained in:
parent
868faeec67
commit
f089932de0
16 changed files with 111 additions and 174 deletions
|
@ -20,7 +20,7 @@ type blobServer struct {
|
||||||
driver driver.StorageDriver
|
driver driver.StorageDriver
|
||||||
statter distribution.BlobStatter
|
statter distribution.BlobStatter
|
||||||
pathFn func(dgst digest.Digest) (string, error)
|
pathFn func(dgst digest.Digest) (string, error)
|
||||||
redirect bool // allows disabling URLFor redirects
|
redirect bool // allows disabling RedirectURL redirects
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bs *blobServer) ServeBlob(ctx context.Context, w http.ResponseWriter, r *http.Request, dgst digest.Digest) error {
|
func (bs *blobServer) ServeBlob(ctx context.Context, w http.ResponseWriter, r *http.Request, dgst digest.Digest) error {
|
||||||
|
@ -35,19 +35,16 @@ func (bs *blobServer) ServeBlob(ctx context.Context, w http.ResponseWriter, r *h
|
||||||
}
|
}
|
||||||
|
|
||||||
if bs.redirect {
|
if bs.redirect {
|
||||||
redirectURL, err := bs.driver.URLFor(ctx, path, map[string]interface{}{"method": r.Method})
|
redirectURL, err := bs.driver.RedirectURL(r, path)
|
||||||
switch err.(type) {
|
if err != nil {
|
||||||
case nil:
|
|
||||||
// Redirect to storage URL.
|
|
||||||
http.Redirect(w, r, redirectURL, http.StatusTemporaryRedirect)
|
|
||||||
return err
|
|
||||||
|
|
||||||
case driver.ErrUnsupportedMethod:
|
|
||||||
// Fallback to serving the content directly.
|
|
||||||
default:
|
|
||||||
// Some unexpected error.
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if redirectURL != "" {
|
||||||
|
// Redirect to storage URL.
|
||||||
|
http.Redirect(w, r, redirectURL, http.StatusTemporaryRedirect)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Fallback to serving the content directly.
|
||||||
}
|
}
|
||||||
|
|
||||||
br, err := newFileReader(ctx, bs.driver, path, desc.Size)
|
br, err := newFileReader(ctx, bs.driver, path, desc.Size)
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -286,7 +287,7 @@ func (d *driver) List(ctx context.Context, path string) ([]string, error) {
|
||||||
// Move moves an object stored at sourcePath to destPath, removing the original
|
// Move moves an object stored at sourcePath to destPath, removing the original
|
||||||
// object.
|
// object.
|
||||||
func (d *driver) Move(ctx context.Context, sourcePath string, destPath string) error {
|
func (d *driver) Move(ctx context.Context, sourcePath string, destPath string) error {
|
||||||
sourceBlobURL, err := d.URLFor(ctx, sourcePath, nil)
|
sourceBlobURL, err := d.signBlobURL(ctx, sourcePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -366,18 +367,15 @@ func (d *driver) Delete(ctx context.Context, path string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// URLFor returns a publicly accessible URL for the blob stored at given path
|
// RedirectURL returns a publicly accessible URL for the blob stored at given path
|
||||||
// for specified duration by making use of Azure Storage Shared Access Signatures (SAS).
|
// for specified duration by making use of Azure Storage Shared Access Signatures (SAS).
|
||||||
// See https://msdn.microsoft.com/en-us/library/azure/ee395415.aspx for more info.
|
// See https://msdn.microsoft.com/en-us/library/azure/ee395415.aspx for more info.
|
||||||
func (d *driver) URLFor(ctx context.Context, path string, options map[string]interface{}) (string, error) {
|
func (d *driver) RedirectURL(req *http.Request, path string) (string, error) {
|
||||||
|
return d.signBlobURL(req.Context(), path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *driver) signBlobURL(ctx context.Context, path string) (string, error) {
|
||||||
expiresTime := time.Now().UTC().Add(20 * time.Minute) // default expiration
|
expiresTime := time.Now().UTC().Add(20 * time.Minute) // default expiration
|
||||||
expires, ok := options["expiry"]
|
|
||||||
if ok {
|
|
||||||
t, ok := expires.(time.Time)
|
|
||||||
if ok {
|
|
||||||
expiresTime = t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
blobName := d.blobName(path)
|
blobName := d.blobName(path)
|
||||||
blobRef := d.client.NewBlobClient(blobName)
|
blobRef := d.client.NewBlobClient(blobName)
|
||||||
return d.azClient.SignBlobURL(ctx, blobRef.URL(), expiresTime)
|
return d.azClient.SignBlobURL(ctx, blobRef.URL(), expiresTime)
|
||||||
|
|
|
@ -40,6 +40,7 @@ package base
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/distribution/distribution/v3/internal/dcontext"
|
"github.com/distribution/distribution/v3/internal/dcontext"
|
||||||
|
@ -208,18 +209,18 @@ func (base *Base) Delete(ctx context.Context, path string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// URLFor wraps URLFor of underlying storage driver.
|
// RedirectURL wraps RedirectURL of the underlying storage driver.
|
||||||
func (base *Base) URLFor(ctx context.Context, path string, options map[string]interface{}) (string, error) {
|
func (base *Base) RedirectURL(r *http.Request, path string) (string, error) {
|
||||||
ctx, done := dcontext.WithTrace(ctx)
|
ctx, done := dcontext.WithTrace(r.Context())
|
||||||
defer done("%s.URLFor(%q)", base.Name(), path)
|
defer done("%s.RedirectURL(%q)", base.Name(), path)
|
||||||
|
|
||||||
if !storagedriver.PathRegexp.MatchString(path) {
|
if !storagedriver.PathRegexp.MatchString(path) {
|
||||||
return "", storagedriver.InvalidPathError{Path: path, DriverName: base.StorageDriver.Name()}
|
return "", storagedriver.InvalidPathError{Path: path, DriverName: base.StorageDriver.Name()}
|
||||||
}
|
}
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
str, e := base.StorageDriver.URLFor(ctx, path, options)
|
str, e := base.StorageDriver.RedirectURL(r.WithContext(ctx), path)
|
||||||
storageAction.WithValues(base.Name(), "URLFor").UpdateSince(start)
|
storageAction.WithValues(base.Name(), "RedirectURL").UpdateSince(start)
|
||||||
return str, base.setDriverName(e)
|
return str, base.setDriverName(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -172,13 +173,11 @@ func (r *regulator) Delete(ctx context.Context, path string) error {
|
||||||
return r.StorageDriver.Delete(ctx, path)
|
return r.StorageDriver.Delete(ctx, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// URLFor returns a URL which may be used to retrieve the content stored at
|
// RedirectURL returns a URL which may be used to retrieve the content stored at
|
||||||
// the given path, possibly using the given options.
|
// the given path.
|
||||||
// May return an ErrUnsupportedMethod in certain StorageDriver
|
func (r *regulator) RedirectURL(req *http.Request, path string) (string, error) {
|
||||||
// implementations.
|
|
||||||
func (r *regulator) URLFor(ctx context.Context, path string, options map[string]interface{}) (string, error) {
|
|
||||||
r.enter()
|
r.enter()
|
||||||
defer r.exit()
|
defer r.exit()
|
||||||
|
|
||||||
return r.StorageDriver.URLFor(ctx, path, options)
|
return r.StorageDriver.RedirectURL(req, path)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"time"
|
"time"
|
||||||
|
@ -282,10 +283,9 @@ func (d *driver) Delete(ctx context.Context, subPath string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// URLFor returns a URL which may be used to retrieve the content stored at the given path.
|
// RedirectURL returns a URL which may be used to retrieve the content stored at the given path.
|
||||||
// May return an UnsupportedMethodErr in certain StorageDriver implementations.
|
func (d *driver) RedirectURL(*http.Request, string) (string, error) {
|
||||||
func (d *driver) URLFor(ctx context.Context, path string, options map[string]interface{}) (string, error) {
|
return "", nil
|
||||||
return "", storagedriver.ErrUnsupportedMethod{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Walk traverses a filesystem defined within driver, starting
|
// Walk traverses a filesystem defined within driver, starting
|
||||||
|
|
|
@ -810,40 +810,24 @@ func storageCopyObject(ctx context.Context, srcBucket, srcName string, destBucke
|
||||||
return attrs, err
|
return attrs, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// URLFor returns a URL which may be used to retrieve the content stored at
|
// RedirectURL returns a URL which may be used to retrieve the content stored at
|
||||||
// the given path, possibly using the given options.
|
// the given path, possibly using the given options.
|
||||||
// Returns ErrUnsupportedMethod if this driver has no privateKey
|
func (d *driver) RedirectURL(r *http.Request, path string) (string, error) {
|
||||||
func (d *driver) URLFor(ctx context.Context, path string, options map[string]interface{}) (string, error) {
|
|
||||||
if d.privateKey == nil {
|
if d.privateKey == nil {
|
||||||
return "", storagedriver.ErrUnsupportedMethod{}
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
name := d.pathToKey(path)
|
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
||||||
methodString := http.MethodGet
|
return "", nil
|
||||||
method, ok := options["method"]
|
|
||||||
if ok {
|
|
||||||
methodString, ok = method.(string)
|
|
||||||
if !ok || (methodString != http.MethodGet && methodString != http.MethodHead) {
|
|
||||||
return "", storagedriver.ErrUnsupportedMethod{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
expiresTime := time.Now().Add(20 * time.Minute)
|
|
||||||
expires, ok := options["expiry"]
|
|
||||||
if ok {
|
|
||||||
et, ok := expires.(time.Time)
|
|
||||||
if ok {
|
|
||||||
expiresTime = et
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := &storage.SignedURLOptions{
|
opts := &storage.SignedURLOptions{
|
||||||
GoogleAccessID: d.email,
|
GoogleAccessID: d.email,
|
||||||
PrivateKey: d.privateKey,
|
PrivateKey: d.privateKey,
|
||||||
Method: methodString,
|
Method: r.Method,
|
||||||
Expires: expiresTime,
|
Expires: time.Now().Add(20 * time.Minute),
|
||||||
}
|
}
|
||||||
return storage.SignedURL(d.bucket, name, opts)
|
return storage.SignedURL(d.bucket, d.pathToKey(path), opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Walk traverses a filesystem defined within driver, starting
|
// Walk traverses a filesystem defined within driver, starting
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -236,10 +237,9 @@ func (d *driver) Delete(ctx context.Context, path string) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// URLFor returns a URL which may be used to retrieve the content stored at the given path.
|
// RedirectURL returns a URL which may be used to retrieve the content stored at the given path.
|
||||||
// May return an UnsupportedMethodErr in certain StorageDriver implementations.
|
func (d *driver) RedirectURL(*http.Request, string) (string, error) {
|
||||||
func (d *driver) URLFor(ctx context.Context, path string, options map[string]interface{}) (string, error) {
|
return "", nil
|
||||||
return "", storagedriver.ErrUnsupportedMethod{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Walk traverses a filesystem defined within driver, starting
|
// Walk traverses a filesystem defined within driver, starting
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -195,18 +196,18 @@ type S3BucketKeyer interface {
|
||||||
S3BucketKey(path string) string
|
S3BucketKey(path string) string
|
||||||
}
|
}
|
||||||
|
|
||||||
// URLFor attempts to find a url which may be used to retrieve the file at the given path.
|
// RedirectURL attempts to find a url which may be used to retrieve the file at the given path.
|
||||||
// Returns an error if the file cannot be found.
|
// Returns an error if the file cannot be found.
|
||||||
func (lh *cloudFrontStorageMiddleware) URLFor(ctx context.Context, path string, options map[string]interface{}) (string, error) {
|
func (lh *cloudFrontStorageMiddleware) RedirectURL(r *http.Request, path string) (string, error) {
|
||||||
// TODO(endophage): currently only supports S3
|
// TODO(endophage): currently only supports S3
|
||||||
keyer, ok := lh.StorageDriver.(S3BucketKeyer)
|
keyer, ok := lh.StorageDriver.(S3BucketKeyer)
|
||||||
if !ok {
|
if !ok {
|
||||||
dcontext.GetLogger(ctx).Warn("the CloudFront middleware does not support this backend storage driver")
|
dcontext.GetLogger(r.Context()).Warn("the CloudFront middleware does not support this backend storage driver")
|
||||||
return lh.StorageDriver.URLFor(ctx, path, options)
|
return lh.StorageDriver.RedirectURL(r, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
if eligibleForS3(ctx, lh.awsIPs) {
|
if eligibleForS3(r, lh.awsIPs) {
|
||||||
return lh.StorageDriver.URLFor(ctx, path, options)
|
return lh.StorageDriver.RedirectURL(r, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get signed cloudfront url.
|
// Get signed cloudfront url.
|
||||||
|
|
|
@ -184,11 +184,7 @@ func (s *awsIPs) contains(ip net.IP) bool {
|
||||||
|
|
||||||
// parseIPFromRequest attempts to extract the ip address of the
|
// parseIPFromRequest attempts to extract the ip address of the
|
||||||
// client that made the request
|
// client that made the request
|
||||||
func parseIPFromRequest(ctx context.Context) (net.IP, error) {
|
func parseIPFromRequest(request *http.Request) (net.IP, error) {
|
||||||
request, err := dcontext.GetRequest(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ipStr := requestutil.RemoteIP(request)
|
ipStr := requestutil.RemoteIP(request)
|
||||||
ip := net.ParseIP(ipStr)
|
ip := net.ParseIP(ipStr)
|
||||||
if ip == nil {
|
if ip == nil {
|
||||||
|
@ -200,25 +196,20 @@ func parseIPFromRequest(ctx context.Context) (net.IP, error) {
|
||||||
|
|
||||||
// eligibleForS3 checks if a request is eligible for using S3 directly
|
// eligibleForS3 checks if a request is eligible for using S3 directly
|
||||||
// Return true only when the IP belongs to a specific aws region and user-agent is docker
|
// Return true only when the IP belongs to a specific aws region and user-agent is docker
|
||||||
func eligibleForS3(ctx context.Context, awsIPs *awsIPs) bool {
|
func eligibleForS3(request *http.Request, awsIPs *awsIPs) bool {
|
||||||
if awsIPs != nil && awsIPs.initialized {
|
if awsIPs != nil && awsIPs.initialized {
|
||||||
if addr, err := parseIPFromRequest(ctx); err == nil {
|
if addr, err := parseIPFromRequest(request); err == nil {
|
||||||
request, err := dcontext.GetRequest(ctx)
|
loggerField := map[interface{}]interface{}{
|
||||||
if err != nil {
|
"user-client": request.UserAgent(),
|
||||||
dcontext.GetLogger(ctx).Warnf("the CloudFront middleware cannot parse the request: %s", err)
|
"ip": requestutil.RemoteIP(request),
|
||||||
} else {
|
|
||||||
loggerField := map[interface{}]interface{}{
|
|
||||||
"user-client": request.UserAgent(),
|
|
||||||
"ip": requestutil.RemoteIP(request),
|
|
||||||
}
|
|
||||||
if awsIPs.contains(addr) {
|
|
||||||
dcontext.GetLoggerWithFields(ctx, loggerField).Info("request from the allowed AWS region, skipping CloudFront")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
dcontext.GetLoggerWithFields(ctx, loggerField).Warn("request not from the allowed AWS region, fallback to CloudFront")
|
|
||||||
}
|
}
|
||||||
|
if awsIPs.contains(addr) {
|
||||||
|
dcontext.GetLoggerWithFields(request.Context(), loggerField).Info("request from the allowed AWS region, skipping CloudFront")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
dcontext.GetLoggerWithFields(request.Context(), loggerField).Warn("request not from the allowed AWS region, fallback to CloudFront")
|
||||||
} else {
|
} else {
|
||||||
dcontext.GetLogger(ctx).WithError(err).Warn("failed to parse ip address from context, fallback to CloudFront")
|
dcontext.GetLogger(request.Context()).WithError(err).Warn("failed to parse ip address from context, fallback to CloudFront")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -11,8 +10,6 @@ import (
|
||||||
"reflect" // used as a replacement for testify
|
"reflect" // used as a replacement for testify
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/distribution/distribution/v3/internal/dcontext"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Rather than pull in all of testify
|
// Rather than pull in all of testify
|
||||||
|
@ -269,29 +266,22 @@ func TestEligibleForS3(t *testing.T) {
|
||||||
}},
|
}},
|
||||||
initialized: true,
|
initialized: true,
|
||||||
}
|
}
|
||||||
empty := context.TODO()
|
|
||||||
makeContext := func(ip string) context.Context {
|
|
||||||
req := &http.Request{
|
|
||||||
RemoteAddr: ip,
|
|
||||||
}
|
|
||||||
|
|
||||||
return dcontext.WithRequest(empty, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
Context context.Context
|
RemoteAddr string
|
||||||
Expected bool
|
Expected bool
|
||||||
}{
|
}{
|
||||||
{Context: empty, Expected: false},
|
{RemoteAddr: "", Expected: false},
|
||||||
{Context: makeContext("192.168.1.2"), Expected: true},
|
{RemoteAddr: "192.168.1.2", Expected: true},
|
||||||
{Context: makeContext("192.168.0.2"), Expected: false},
|
{RemoteAddr: "192.168.0.2", Expected: false},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
tc := tc
|
tc := tc
|
||||||
t.Run(fmt.Sprintf("Client IP = %v", tc.Context.Value("http.request.ip")), func(t *testing.T) {
|
t.Run(fmt.Sprintf("Client IP = %v", tc.RemoteAddr), func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
assertEqual(t, tc.Expected, eligibleForS3(tc.Context, ips))
|
req := &http.Request{RemoteAddr: tc.RemoteAddr}
|
||||||
|
assertEqual(t, tc.Expected, eligibleForS3(req, ips))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -305,29 +295,22 @@ func TestEligibleForS3WithAWSIPNotInitialized(t *testing.T) {
|
||||||
}},
|
}},
|
||||||
initialized: false,
|
initialized: false,
|
||||||
}
|
}
|
||||||
empty := context.TODO()
|
|
||||||
makeContext := func(ip string) context.Context {
|
|
||||||
req := &http.Request{
|
|
||||||
RemoteAddr: ip,
|
|
||||||
}
|
|
||||||
|
|
||||||
return dcontext.WithRequest(empty, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
Context context.Context
|
RemoteAddr string
|
||||||
Expected bool
|
Expected bool
|
||||||
}{
|
}{
|
||||||
{Context: empty, Expected: false},
|
{RemoteAddr: "", Expected: false},
|
||||||
{Context: makeContext("192.168.1.2"), Expected: false},
|
{RemoteAddr: "192.168.1.2", Expected: false},
|
||||||
{Context: makeContext("192.168.0.2"), Expected: false},
|
{RemoteAddr: "192.168.0.2", Expected: false},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
tc := tc
|
tc := tc
|
||||||
t.Run(fmt.Sprintf("Client IP = %v", tc.Context.Value("http.request.ip")), func(t *testing.T) {
|
t.Run(fmt.Sprintf("Client IP = %v", tc.RemoteAddr), func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
assertEqual(t, tc.Expected, eligibleForS3(tc.Context, ips))
|
req := &http.Request{RemoteAddr: tc.RemoteAddr}
|
||||||
|
assertEqual(t, tc.Expected, eligibleForS3(req, ips))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ func newRedirectStorageMiddleware(sd storagedriver.StorageDriver, options map[st
|
||||||
return &redirectStorageMiddleware{StorageDriver: sd, scheme: u.Scheme, host: u.Host, basePath: u.Path}, nil
|
return &redirectStorageMiddleware{StorageDriver: sd, scheme: u.Scheme, host: u.Host, basePath: u.Path}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *redirectStorageMiddleware) URLFor(ctx context.Context, urlPath string, options map[string]interface{}) (string, error) {
|
func (r *redirectStorageMiddleware) RedirectURL(_ *http.Request, urlPath string) (string, error) {
|
||||||
if r.basePath != "" {
|
if r.basePath != "" {
|
||||||
urlPath = path.Join(r.basePath, urlPath)
|
urlPath = path.Join(r.basePath, urlPath)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gopkg.in/check.v1"
|
"gopkg.in/check.v1"
|
||||||
|
@ -37,7 +36,7 @@ func (s *MiddlewareSuite) TestHttpsPort(c *check.C) {
|
||||||
c.Assert(m.scheme, check.Equals, "https")
|
c.Assert(m.scheme, check.Equals, "https")
|
||||||
c.Assert(m.host, check.Equals, "example.com:5443")
|
c.Assert(m.host, check.Equals, "example.com:5443")
|
||||||
|
|
||||||
url, err := middleware.URLFor(context.TODO(), "/rick/data", nil)
|
url, err := middleware.RedirectURL(nil, "/rick/data")
|
||||||
c.Assert(err, check.Equals, nil)
|
c.Assert(err, check.Equals, nil)
|
||||||
c.Assert(url, check.Equals, "https://example.com:5443/rick/data")
|
c.Assert(url, check.Equals, "https://example.com:5443/rick/data")
|
||||||
}
|
}
|
||||||
|
@ -53,7 +52,7 @@ func (s *MiddlewareSuite) TestHTTP(c *check.C) {
|
||||||
c.Assert(m.scheme, check.Equals, "http")
|
c.Assert(m.scheme, check.Equals, "http")
|
||||||
c.Assert(m.host, check.Equals, "example.com")
|
c.Assert(m.host, check.Equals, "example.com")
|
||||||
|
|
||||||
url, err := middleware.URLFor(context.TODO(), "morty/data", nil)
|
url, err := middleware.RedirectURL(nil, "morty/data")
|
||||||
c.Assert(err, check.Equals, nil)
|
c.Assert(err, check.Equals, nil)
|
||||||
c.Assert(url, check.Equals, "http://example.com/morty/data")
|
c.Assert(url, check.Equals, "http://example.com/morty/data")
|
||||||
}
|
}
|
||||||
|
@ -71,12 +70,12 @@ func (s *MiddlewareSuite) TestPath(c *check.C) {
|
||||||
c.Assert(m.host, check.Equals, "example.com")
|
c.Assert(m.host, check.Equals, "example.com")
|
||||||
c.Assert(m.basePath, check.Equals, "/path")
|
c.Assert(m.basePath, check.Equals, "/path")
|
||||||
|
|
||||||
// call URLFor() with no leading slash
|
// call RedirectURL() with no leading slash
|
||||||
url, err := middleware.URLFor(context.TODO(), "morty/data", nil)
|
url, err := middleware.RedirectURL(nil, "morty/data")
|
||||||
c.Assert(err, check.Equals, nil)
|
c.Assert(err, check.Equals, nil)
|
||||||
c.Assert(url, check.Equals, "https://example.com/path/morty/data")
|
c.Assert(url, check.Equals, "https://example.com/path/morty/data")
|
||||||
// call URLFor() with leading slash
|
// call RedirectURL() with leading slash
|
||||||
url, err = middleware.URLFor(context.TODO(), "/morty/data", nil)
|
url, err = middleware.RedirectURL(nil, "/morty/data")
|
||||||
c.Assert(err, check.Equals, nil)
|
c.Assert(err, check.Equals, nil)
|
||||||
c.Assert(url, check.Equals, "https://example.com/path/morty/data")
|
c.Assert(url, check.Equals, "https://example.com/path/morty/data")
|
||||||
|
|
||||||
|
@ -91,12 +90,12 @@ func (s *MiddlewareSuite) TestPath(c *check.C) {
|
||||||
c.Assert(m.host, check.Equals, "example.com")
|
c.Assert(m.host, check.Equals, "example.com")
|
||||||
c.Assert(m.basePath, check.Equals, "/path/")
|
c.Assert(m.basePath, check.Equals, "/path/")
|
||||||
|
|
||||||
// call URLFor() with no leading slash
|
// call RedirectURL() with no leading slash
|
||||||
url, err = middleware.URLFor(context.TODO(), "morty/data", nil)
|
url, err = middleware.RedirectURL(nil, "morty/data")
|
||||||
c.Assert(err, check.Equals, nil)
|
c.Assert(err, check.Equals, nil)
|
||||||
c.Assert(url, check.Equals, "https://example.com/path/morty/data")
|
c.Assert(url, check.Equals, "https://example.com/path/morty/data")
|
||||||
// call URLFor() with leading slash
|
// call RedirectURL() with leading slash
|
||||||
url, err = middleware.URLFor(context.TODO(), "/morty/data", nil)
|
url, err = middleware.RedirectURL(nil, "/morty/data")
|
||||||
c.Assert(err, check.Equals, nil)
|
c.Assert(err, check.Equals, nil)
|
||||||
c.Assert(url, check.Equals, "https://example.com/path/morty/data")
|
c.Assert(url, check.Equals, "https://example.com/path/morty/data")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1036,30 +1036,13 @@ func (d *driver) Delete(ctx context.Context, path string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// URLFor returns a URL which may be used to retrieve the content stored at the given path.
|
// RedirectURL returns a URL which may be used to retrieve the content stored at the given path.
|
||||||
// May return an UnsupportedMethodErr in certain StorageDriver implementations.
|
func (d *driver) RedirectURL(r *http.Request, path string) (string, error) {
|
||||||
func (d *driver) URLFor(ctx context.Context, path string, options map[string]interface{}) (string, error) {
|
|
||||||
methodString := http.MethodGet
|
|
||||||
method, ok := options["method"]
|
|
||||||
if ok {
|
|
||||||
methodString, ok = method.(string)
|
|
||||||
if !ok || (methodString != http.MethodGet && methodString != http.MethodHead) {
|
|
||||||
return "", storagedriver.ErrUnsupportedMethod{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
expiresIn := 20 * time.Minute
|
expiresIn := 20 * time.Minute
|
||||||
expires, ok := options["expiry"]
|
|
||||||
if ok {
|
|
||||||
et, ok := expires.(time.Time)
|
|
||||||
if ok {
|
|
||||||
expiresIn = time.Until(et)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var req *request.Request
|
var req *request.Request
|
||||||
|
|
||||||
switch methodString {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
req, _ = d.S3.GetObjectRequest(&s3.GetObjectInput{
|
req, _ = d.S3.GetObjectRequest(&s3.GetObjectInput{
|
||||||
Bucket: aws.String(d.Bucket),
|
Bucket: aws.String(d.Bucket),
|
||||||
|
@ -1071,7 +1054,7 @@ func (d *driver) URLFor(ctx context.Context, path string, options map[string]int
|
||||||
Key: aws.String(d.s3Path(path)),
|
Key: aws.String(d.s3Path(path)),
|
||||||
})
|
})
|
||||||
default:
|
default:
|
||||||
panic("unreachable")
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return req.Presign(expiresIn)
|
return req.Presign(expiresIn)
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -92,11 +93,10 @@ type StorageDriver interface {
|
||||||
// Delete recursively deletes all objects stored at "path" and its subpaths.
|
// Delete recursively deletes all objects stored at "path" and its subpaths.
|
||||||
Delete(ctx context.Context, path string) error
|
Delete(ctx context.Context, path string) error
|
||||||
|
|
||||||
// URLFor returns a URL which may be used to retrieve the content stored at
|
// RedirectURL returns a URL which the client of the request r may use
|
||||||
// the given path, possibly using the given options.
|
// to retrieve the content stored at path. Returning the empty string
|
||||||
// May return an ErrUnsupportedMethod in certain StorageDriver
|
// signals that the request may not be redirected.
|
||||||
// implementations.
|
RedirectURL(r *http.Request, path string) (string, error)
|
||||||
URLFor(ctx context.Context, path string, options map[string]interface{}) (string, error)
|
|
||||||
|
|
||||||
// Walk traverses a filesystem defined within driver, starting
|
// Walk traverses a filesystem defined within driver, starting
|
||||||
// from the given path, calling f on each file.
|
// from the given path, calling f on each file.
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"sort"
|
"sort"
|
||||||
|
@ -733,9 +734,9 @@ func (suite *DriverSuite) TestDelete(c *check.C) {
|
||||||
c.Assert(strings.Contains(err.Error(), suite.Name()), check.Equals, true)
|
c.Assert(strings.Contains(err.Error(), suite.Name()), check.Equals, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestURLFor checks that the URLFor method functions properly, but only if it
|
// TestRedirectURL checks that the RedirectURL method functions properly,
|
||||||
// is implemented
|
// but only if it is implemented
|
||||||
func (suite *DriverSuite) TestURLFor(c *check.C) {
|
func (suite *DriverSuite) TestRedirectURL(c *check.C) {
|
||||||
filename := randomPath(32)
|
filename := randomPath(32)
|
||||||
contents := randomContents(32)
|
contents := randomContents(32)
|
||||||
|
|
||||||
|
@ -744,8 +745,8 @@ func (suite *DriverSuite) TestURLFor(c *check.C) {
|
||||||
err := suite.StorageDriver.PutContent(suite.ctx, filename, contents)
|
err := suite.StorageDriver.PutContent(suite.ctx, filename, contents)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
url, err := suite.StorageDriver.URLFor(suite.ctx, filename, nil)
|
url, err := suite.StorageDriver.RedirectURL(httptest.NewRequest(http.MethodGet, filename, nil), filename)
|
||||||
if _, ok := err.(storagedriver.ErrUnsupportedMethod); ok {
|
if url == "" && err == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
@ -758,8 +759,8 @@ func (suite *DriverSuite) TestURLFor(c *check.C) {
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
c.Assert(read, check.DeepEquals, contents)
|
c.Assert(read, check.DeepEquals, contents)
|
||||||
|
|
||||||
url, err = suite.StorageDriver.URLFor(suite.ctx, filename, map[string]interface{}{"method": http.MethodHead})
|
url, err = suite.StorageDriver.RedirectURL(httptest.NewRequest(http.MethodHead, filename, nil), filename)
|
||||||
if _, ok := err.(storagedriver.ErrUnsupportedMethod); ok {
|
if url == "" && err == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
|
@ -34,7 +34,7 @@ type manifestURLs struct {
|
||||||
type RegistryOption func(*registry) error
|
type RegistryOption func(*registry) error
|
||||||
|
|
||||||
// EnableRedirect is a functional option for NewRegistry. It causes the backend
|
// EnableRedirect is a functional option for NewRegistry. It causes the backend
|
||||||
// blob server to attempt using (StorageDriver).URLFor to serve all blobs.
|
// blob server to attempt using (StorageDriver).RedirectURL to serve all blobs.
|
||||||
func EnableRedirect(registry *registry) error {
|
func EnableRedirect(registry *registry) error {
|
||||||
registry.blobServer.redirect = true
|
registry.blobServer.redirect = true
|
||||||
return nil
|
return nil
|
||||||
|
@ -102,7 +102,7 @@ func BlobDescriptorCacheProvider(blobDescriptorCacheProvider cache.BlobDescripto
|
||||||
// NewRegistry creates a new registry instance from the provided driver. The
|
// NewRegistry creates a new registry instance from the provided driver. The
|
||||||
// resulting registry may be shared by multiple goroutines but is cheap to
|
// resulting registry may be shared by multiple goroutines but is cheap to
|
||||||
// allocate. If the Redirect option is specified, the backend blob server will
|
// allocate. If the Redirect option is specified, the backend blob server will
|
||||||
// attempt to use (StorageDriver).URLFor to serve all blobs.
|
// attempt to use (StorageDriver).RedirectURL to serve all blobs.
|
||||||
func NewRegistry(ctx context.Context, driver storagedriver.StorageDriver, options ...RegistryOption) (distribution.Namespace, error) {
|
func NewRegistry(ctx context.Context, driver storagedriver.StorageDriver, options ...RegistryOption) (distribution.Namespace, error) {
|
||||||
// create global statter
|
// create global statter
|
||||||
statter := &blobStatter{
|
statter := &blobStatter{
|
||||||
|
|
Loading…
Reference in a new issue