proxymode/proxymode.go (168 lines of code) (raw):
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License 2.0;
// you may not use this file except in compliance with the Elastic License 2.0.
package proxymode
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/gorilla/mux"
"github.com/hashicorp/go-retryablehttp"
"go.uber.org/zap"
"github.com/elastic/package-registry/packages"
)
type ProxyMode struct {
options ProxyOptions
httpClient *retryablehttp.Client
destinationURL *url.URL
resolver *proxyResolver
logger *zap.Logger
}
type ProxyOptions struct {
Enabled bool
ProxyTo string
}
func NoProxy(logger *zap.Logger) *ProxyMode {
proxyMode, err := NewProxyMode(logger, ProxyOptions{Enabled: false})
if err != nil {
panic(fmt.Errorf("unexpected error: %w", err))
}
return proxyMode
}
func NewProxyMode(logger *zap.Logger, options ProxyOptions) (*ProxyMode, error) {
var pm ProxyMode
pm.options = options
pm.logger = logger
if !options.Enabled {
return &pm, nil
}
pm.httpClient = &retryablehttp.Client{
HTTPClient: &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
},
},
Logger: withZapLoggerAdapter(logger),
RetryWaitMin: 1 * time.Second,
RetryWaitMax: 15 * time.Second,
RetryMax: 4,
CheckRetry: proxyRetryPolicy,
Backoff: retryablehttp.DefaultBackoff,
}
var err error
pm.destinationURL, err = url.Parse(pm.options.ProxyTo)
if err != nil {
return nil, fmt.Errorf("can't create proxy destination URL: %w", err)
}
pm.resolver = &proxyResolver{destinationURL: *pm.destinationURL}
return &pm, nil
}
// proxyRetryPolicy function extends the DefaultRetryPolicy to check if the HTTP response content-type
// is application/json. We found occurrences of requests being rejected by an intermittent proxy and causing
// the json.Decoder to fail.
func proxyRetryPolicy(ctx context.Context, resp *http.Response, err error) (bool, error) {
shouldRetry, err := retryablehttp.DefaultRetryPolicy(ctx, resp, err)
if shouldRetry {
return shouldRetry, err
}
// Chaining Package Registry servers (proxies) is allowed. HTTP client must get to the end of the chain.
locationHeader := resp.Header.Get("location")
if locationHeader != "" {
return false, nil
}
// Expect json content type only for success statuses.
if code := resp.StatusCode; code >= 200 && code < 300 {
contentType := resp.Header.Get("content-type")
if !strings.HasPrefix(contentType, "application/json") {
return true, fmt.Errorf("unexpected content type: %s", contentType)
}
}
return false, nil
}
func (pm *ProxyMode) Enabled() bool {
return pm.options.Enabled
}
func (pm *ProxyMode) Search(r *http.Request) (packages.Packages, error) {
proxyURL := *r.URL
proxyURL.Host = pm.destinationURL.Host
proxyURL.Scheme = pm.destinationURL.Scheme
proxyURL.User = pm.destinationURL.User
proxyRequest, err := retryablehttp.NewRequest(http.MethodGet, proxyURL.String(), nil)
if err != nil {
return nil, fmt.Errorf("can't create proxy request: %w", err)
}
pm.logger.Debug("Proxy /search request", zap.String("request.uri", proxyURL.String()))
response, err := pm.httpClient.Do(proxyRequest)
if err != nil {
return nil, fmt.Errorf("can't proxy search request: %w", err)
}
defer response.Body.Close()
var pkgs packages.Packages
err = json.NewDecoder(response.Body).Decode(&pkgs)
if err != nil {
return nil, fmt.Errorf("can't proxy search request: %w", err)
}
for i := 0; i < len(pkgs); i++ {
pkgs[i].SetRemoteResolver(pm.resolver)
}
return pkgs, nil
}
func (pm *ProxyMode) Categories(r *http.Request) ([]packages.Category, error) {
proxyURL := *r.URL
proxyURL.Host = pm.destinationURL.Host
proxyURL.Scheme = pm.destinationURL.Scheme
proxyURL.User = pm.destinationURL.User
proxyRequest, err := retryablehttp.NewRequest(http.MethodGet, proxyURL.String(), nil)
if err != nil {
return nil, fmt.Errorf("can't create proxy request: %w", err)
}
pm.logger.Debug("Proxy /categories request", zap.String("request.uri", proxyURL.String()))
response, err := pm.httpClient.Do(proxyRequest)
if err != nil {
return nil, fmt.Errorf("can't proxy categories request: %w", err)
}
defer response.Body.Close()
var cats []packages.Category
err = json.NewDecoder(response.Body).Decode(&cats)
if err != nil {
return nil, fmt.Errorf("can't proxy categories request: %w", err)
}
return cats, nil
}
func (pm *ProxyMode) Package(r *http.Request) (*packages.Package, error) {
vars := mux.Vars(r)
packageName, ok := vars["packageName"]
if !ok {
return nil, errors.New("missing package name")
}
packageVersion, ok := vars["packageVersion"]
if !ok {
return nil, errors.New("missing package version")
}
urlPath := fmt.Sprintf("/package/%s/%s/", packageName, packageVersion)
proxyURL := pm.destinationURL.ResolveReference(&url.URL{Path: urlPath})
proxyRequest, err := retryablehttp.NewRequest(http.MethodGet, proxyURL.String(), nil)
if err != nil {
return nil, fmt.Errorf("can't create proxy request: %w", err)
}
pm.logger.Debug("Proxy /package request", zap.String("request.uri", proxyURL.String()))
response, err := pm.httpClient.Do(proxyRequest)
if err != nil {
return nil, fmt.Errorf("can't proxy package request: %w", err)
}
defer response.Body.Close()
switch response.StatusCode {
case http.StatusOK:
// Package found, all good.
case http.StatusNotFound:
// Package doesn't exist, don't try to parse the response, just return an empty package.
return nil, nil
default:
return nil, fmt.Errorf("unexpected status code %d received", response.StatusCode)
}
var pkg packages.Package
err = json.NewDecoder(response.Body).Decode(&pkg)
if err != nil {
return nil, fmt.Errorf("can't proxy package request: %w", err)
}
pkg.SetRemoteResolver(pm.resolver)
return &pkg, nil
}