registry/storage/driver/middleware/googlecdn/middleware.go (163 lines of code) (raw):

// Package googlecdn provides a Google CDN middleware wrapper for the Google Cloud Storage (GCS) storage driver. package googlecdn import ( "context" "fmt" "net/url" "strings" "time" "github.com/benbjohnson/clock" "github.com/docker/distribution/log" "github.com/docker/distribution/registry/internal" dstorage "github.com/docker/distribution/registry/storage" "github.com/docker/distribution/registry/storage/driver" storagemiddleware "github.com/docker/distribution/registry/storage/driver/middleware" "github.com/docker/distribution/registry/storage/internal/metrics" ) // googleCDNStorageMiddleware provides a simple implementation of driver.StorageDriver that constructs temporary // signed Google CDN URLs for the GCS storage driver layer URL, then issues HTTP Temporary Redirects to this content URL. type googleCDNStorageMiddleware struct { driver.StorageDriver googleIPs *googleIPs urlSigner *urlSigner baseURL string duration time.Duration } var _ driver.StorageDriver = &googleCDNStorageMiddleware{} // defaultDuration is the default expiration delay for CDN signed URLs const defaultDuration = 20 * time.Minute // customGitlabGoogle... are the query params appended to googlecdn signed redirect url const ( customGitlabGoogleNamespaceIdParam = "gitlab-namespace-id" customGitlabGoogleProjectIdParam = "gitlab-project-id" customGitlabGoogleAuthTypeParam = "gitlab-auth-type" customGitlabGoogleObjectSizeParam = "gitlab-size-bytes" ) // customParamKeys is the mapping between gitlab keys to googlecdn signed-redirect-url query parameter keys var customParamKeys = map[string]string{ dstorage.NamespaceIdKey: customGitlabGoogleNamespaceIdParam, dstorage.ProjectIdKey: customGitlabGoogleProjectIdParam, dstorage.AuthTypeKey: customGitlabGoogleAuthTypeParam, dstorage.SizeBytesKey: customGitlabGoogleObjectSizeParam, } // newGoogleCDNStorageMiddleware constructs and returns a new Google CDN driver.StorageDriver implementation. // Required options: baseurl, authtype, privatekey, keyname // Optional options: duration, updatefrequency, iprangesurl, ipfilteredby func newGoogleCDNStorageMiddleware(storageDriver driver.StorageDriver, options map[string]any) (driver.StorageDriver, error) { // parse baseurl base, ok := options["baseurl"] if !ok { return nil, fmt.Errorf("no baseurl provided") } baseURL, ok := base.(string) if !ok { return nil, fmt.Errorf("baseurl must be a string") } if !strings.Contains(baseURL, "://") { baseURL = "https://" + baseURL } if !strings.HasSuffix(baseURL, "/") { baseURL += "/" } if _, err := url.Parse(baseURL); err != nil { return nil, fmt.Errorf("invalid baseurl: %v", err) } // parse privatekey pk, ok := options["privatekey"] if !ok { return nil, fmt.Errorf("no privatekey provided") } keyName, ok := pk.(string) if !ok { return nil, fmt.Errorf("privatekey must be a string") } key, err := readKeyFile(keyName) if err != nil { return nil, fmt.Errorf("failed to read privatekey file: %s", err) } // parse keyname v, ok := options["keyname"] if !ok { return nil, fmt.Errorf("no keyname provided") } pkName, ok := v.(string) if !ok { return nil, fmt.Errorf("keyname must be a string") } urlSigner := newURLSigner(pkName, key) // parse duration duration := defaultDuration if d, ok := options["duration"]; ok { switch d := d.(type) { case time.Duration: duration = d case string: dur, err := time.ParseDuration(d) if err != nil { return nil, fmt.Errorf("invalid duration: %s", err) } duration = dur } } // parse updatefrequency updateFrequency := defaultUpdateFrequency if v, ok := options["updatefrequency"]; ok { switch v := v.(type) { case time.Duration: updateFrequency = v case string: d, err := time.ParseDuration(v) if err != nil { return nil, fmt.Errorf("invalid updatefrequency: %s", err) } updateFrequency = d } } // parse iprangesurl ipRangesURL := defaultIPRangesURL if v, ok := options["iprangesurl"]; ok { s, ok := v.(string) if !ok { return nil, fmt.Errorf("iprangesurl must be a string") } ipRangesURL = s } // parse ipfilteredby var googleIPs *googleIPs if v, ok := options["ipfilteredby"]; ok { ipFilteredBy, ok := v.(string) if !ok { return nil, fmt.Errorf("ipfilteredby must be a string") } switch strings.ToLower(strings.TrimSpace(ipFilteredBy)) { case "", "none": googleIPs = nil case "gcp": googleIPs = newGoogleIPs(ipRangesURL, updateFrequency) default: return nil, fmt.Errorf("ipfilteredby must be one of the following values: none|gcp") } } return &googleCDNStorageMiddleware{ StorageDriver: storageDriver, urlSigner: urlSigner, baseURL: baseURL, duration: duration, googleIPs: googleIPs, }, nil } // for testing purposes var systemClock internal.Clock = clock.New() // gcsBucketKeyer is any type that is capable of returning the GCS bucket key which should be cached by Google CDN. type gcsBucketKeyer interface { GCSBucketKey(path string) string } // URLFor returns a URL which may be used to retrieve the content stored at the given path, possibly using the given // options. func (lh *googleCDNStorageMiddleware) URLFor(ctx context.Context, path string, options map[string]any) (string, error) { l := log.GetLogger(log.WithContext(ctx)) keyer, ok := lh.StorageDriver.(gcsBucketKeyer) if !ok { l.Warn("the Google CDN middleware does not support this backend storage driver, bypassing") metrics.CDNRedirect("gcs", true, "unsupported") return lh.StorageDriver.URLFor(ctx, path, options) } if eligibleForGCS(ctx, lh.googleIPs) { metrics.CDNRedirect("gcs", true, "gcp") return lh.StorageDriver.URLFor(ctx, path, options) } metrics.CDNRedirect("cdn", false, "") fullURL := lh.baseURL + keyer.GCSBucketKey(path) // sign the url fullURL, err := lh.urlSigner.Sign(fullURL, systemClock.Now().Add(lh.duration)) if err != nil { return fullURL, err } // add custom params customQueryParams := driver.CustomParams(options, customParamKeys) if len(customQueryParams) != 0 { fullURL = fullURL + "&" + customQueryParams.Encode() } return fullURL, err } // init registers the Google CDN middleware. func init() { // nolint: gosec // ignore when backend is already registered _ = storagemiddleware.Register("googlecdn", newGoogleCDNStorageMiddleware) }