github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/getproviders/http_mirror_source.go (about)

     1  package getproviders
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"log"
     9  	"mime"
    10  	"net/http"
    11  	"net/url"
    12  	"path"
    13  	"strings"
    14  
    15  	"github.com/hashicorp/go-retryablehttp"
    16  	svchost "github.com/hashicorp/terraform-svchost"
    17  	svcauth "github.com/hashicorp/terraform-svchost/auth"
    18  	"golang.org/x/net/idna"
    19  
    20  	"github.com/hashicorp/terraform/internal/addrs"
    21  	"github.com/hashicorp/terraform/internal/httpclient"
    22  	"github.com/hashicorp/terraform/internal/logging"
    23  	"github.com/hashicorp/terraform/version"
    24  )
    25  
    26  // HTTPMirrorSource is a source that reads provider metadata from a provider
    27  // mirror that is accessible over the HTTP provider mirror protocol.
    28  type HTTPMirrorSource struct {
    29  	baseURL    *url.URL
    30  	creds      svcauth.CredentialsSource
    31  	httpClient *retryablehttp.Client
    32  }
    33  
    34  var _ Source = (*HTTPMirrorSource)(nil)
    35  
    36  // NewHTTPMirrorSource constructs and returns a new network mirror source with
    37  // the given base URL. The relative URL offsets defined by the HTTP mirror
    38  // protocol will be resolve relative to the given URL.
    39  //
    40  // The given URL must use the "https" scheme, or this function will panic.
    41  // (When the URL comes from user input, such as in the CLI config, it's the
    42  // UI/config layer's responsibility to validate this and return a suitable
    43  // error message for the end-user audience.)
    44  func NewHTTPMirrorSource(baseURL *url.URL, creds svcauth.CredentialsSource) *HTTPMirrorSource {
    45  	httpClient := httpclient.New()
    46  	httpClient.Timeout = requestTimeout
    47  	httpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
    48  		// If we get redirected more than five times we'll assume we're
    49  		// in a redirect loop and bail out, rather than hanging forever.
    50  		if len(via) > 5 {
    51  			return fmt.Errorf("too many redirects")
    52  		}
    53  		return nil
    54  	}
    55  	return newHTTPMirrorSourceWithHTTPClient(baseURL, creds, httpClient)
    56  }
    57  
    58  func newHTTPMirrorSourceWithHTTPClient(baseURL *url.URL, creds svcauth.CredentialsSource, httpClient *http.Client) *HTTPMirrorSource {
    59  	if baseURL.Scheme != "https" {
    60  		panic("non-https URL for HTTP mirror")
    61  	}
    62  
    63  	// We borrow the retry settings and behaviors from the registry client,
    64  	// because our needs here are very similar to those of the registry client.
    65  	retryableClient := retryablehttp.NewClient()
    66  	retryableClient.HTTPClient = httpClient
    67  	retryableClient.RetryMax = discoveryRetry
    68  	retryableClient.RequestLogHook = requestLogHook
    69  	retryableClient.ErrorHandler = maxRetryErrorHandler
    70  
    71  	retryableClient.Logger = log.New(logging.LogOutput(), "", log.Flags())
    72  
    73  	return &HTTPMirrorSource{
    74  		baseURL:    baseURL,
    75  		creds:      creds,
    76  		httpClient: retryableClient,
    77  	}
    78  }
    79  
    80  // AvailableVersions retrieves the available versions for the given provider
    81  // from the object's underlying HTTP mirror service.
    82  func (s *HTTPMirrorSource) AvailableVersions(ctx context.Context, provider addrs.Provider) (VersionList, Warnings, error) {
    83  	log.Printf("[DEBUG] Querying available versions of provider %s at network mirror %s", provider.String(), s.baseURL.String())
    84  
    85  	endpointPath := path.Join(
    86  		provider.Hostname.String(),
    87  		provider.Namespace,
    88  		provider.Type,
    89  		"index.json",
    90  	)
    91  
    92  	statusCode, body, finalURL, err := s.get(ctx, endpointPath)
    93  	defer func() {
    94  		if body != nil {
    95  			body.Close()
    96  		}
    97  	}()
    98  	if err != nil {
    99  		return nil, nil, s.errQueryFailed(provider, err)
   100  	}
   101  
   102  	switch statusCode {
   103  	case http.StatusOK:
   104  		// Great!
   105  	case http.StatusNotFound:
   106  		return nil, nil, ErrProviderNotFound{
   107  			Provider: provider,
   108  		}
   109  	case http.StatusUnauthorized, http.StatusForbidden:
   110  		return nil, nil, s.errUnauthorized(finalURL)
   111  	default:
   112  		return nil, nil, s.errQueryFailed(provider, fmt.Errorf("server returned unsuccessful status %d", statusCode))
   113  	}
   114  
   115  	// If we got here then the response had status OK and so our body
   116  	// will be non-nil and should contain some JSON for us to parse.
   117  	type ResponseBody struct {
   118  		Versions map[string]struct{} `json:"versions"`
   119  	}
   120  	var bodyContent ResponseBody
   121  
   122  	dec := json.NewDecoder(body)
   123  	if err := dec.Decode(&bodyContent); err != nil {
   124  		return nil, nil, s.errQueryFailed(provider, fmt.Errorf("invalid response content from mirror server: %s", err))
   125  	}
   126  
   127  	if len(bodyContent.Versions) == 0 {
   128  		return nil, nil, nil
   129  	}
   130  	ret := make(VersionList, 0, len(bodyContent.Versions))
   131  	for versionStr := range bodyContent.Versions {
   132  		version, err := ParseVersion(versionStr)
   133  		if err != nil {
   134  			log.Printf("[WARN] Ignoring invalid %s version string %q in provider mirror response", provider, versionStr)
   135  			continue
   136  		}
   137  		ret = append(ret, version)
   138  	}
   139  
   140  	ret.Sort()
   141  	return ret, nil, nil
   142  }
   143  
   144  // PackageMeta retrieves metadata for the requested provider package
   145  // from the object's underlying HTTP mirror service.
   146  func (s *HTTPMirrorSource) PackageMeta(ctx context.Context, provider addrs.Provider, version Version, target Platform) (PackageMeta, error) {
   147  	log.Printf("[DEBUG] Finding package URL for %s v%s on %s via network mirror %s", provider.String(), version.String(), target.String(), s.baseURL.String())
   148  
   149  	endpointPath := path.Join(
   150  		provider.Hostname.String(),
   151  		provider.Namespace,
   152  		provider.Type,
   153  		version.String()+".json",
   154  	)
   155  
   156  	statusCode, body, finalURL, err := s.get(ctx, endpointPath)
   157  	defer func() {
   158  		if body != nil {
   159  			body.Close()
   160  		}
   161  	}()
   162  	if err != nil {
   163  		return PackageMeta{}, s.errQueryFailed(provider, err)
   164  	}
   165  
   166  	switch statusCode {
   167  	case http.StatusOK:
   168  		// Great!
   169  	case http.StatusNotFound:
   170  		// A 404 Not Found for a version we previously saw in index.json is
   171  		// a protocol error, so we'll report this as "query failed.
   172  		return PackageMeta{}, s.errQueryFailed(provider, fmt.Errorf("provider mirror does not have archive index for previously-reported %s version %s", provider, version))
   173  	case http.StatusUnauthorized, http.StatusForbidden:
   174  		return PackageMeta{}, s.errUnauthorized(finalURL)
   175  	default:
   176  		return PackageMeta{}, s.errQueryFailed(provider, fmt.Errorf("server returned unsuccessful status %d", statusCode))
   177  	}
   178  
   179  	// If we got here then the response had status OK and so our body
   180  	// will be non-nil and should contain some JSON for us to parse.
   181  	type ResponseArchiveMeta struct {
   182  		RelativeURL string `json:"url"`
   183  		Hashes      []string
   184  	}
   185  	type ResponseBody struct {
   186  		Archives map[string]*ResponseArchiveMeta `json:"archives"`
   187  	}
   188  	var bodyContent ResponseBody
   189  
   190  	dec := json.NewDecoder(body)
   191  	if err := dec.Decode(&bodyContent); err != nil {
   192  		return PackageMeta{}, s.errQueryFailed(provider, fmt.Errorf("invalid response content from mirror server: %s", err))
   193  	}
   194  
   195  	archiveMeta, ok := bodyContent.Archives[target.String()]
   196  	if !ok {
   197  		return PackageMeta{}, ErrPlatformNotSupported{
   198  			Provider:  provider,
   199  			Version:   version,
   200  			Platform:  target,
   201  			MirrorURL: s.baseURL,
   202  		}
   203  	}
   204  
   205  	relURL, err := url.Parse(archiveMeta.RelativeURL)
   206  	if err != nil {
   207  		return PackageMeta{}, s.errQueryFailed(
   208  			provider,
   209  			fmt.Errorf("provider mirror returned invalid URL %q: %s", archiveMeta.RelativeURL, err),
   210  		)
   211  	}
   212  	absURL := finalURL.ResolveReference(relURL)
   213  
   214  	ret := PackageMeta{
   215  		Provider:       provider,
   216  		Version:        version,
   217  		TargetPlatform: target,
   218  
   219  		Location: PackageHTTPURL(absURL.String()),
   220  		Filename: path.Base(absURL.Path),
   221  	}
   222  	// A network mirror might not provide any hashes at all, in which case
   223  	// the package has no source-defined authentication whatsoever.
   224  	if len(archiveMeta.Hashes) > 0 {
   225  		hashes := make([]Hash, 0, len(archiveMeta.Hashes))
   226  		for _, hashStr := range archiveMeta.Hashes {
   227  			hash, err := ParseHash(hashStr)
   228  			if err != nil {
   229  				return PackageMeta{}, s.errQueryFailed(
   230  					provider,
   231  					fmt.Errorf("provider mirror returned invalid provider hash %q: %s", hashStr, err),
   232  				)
   233  			}
   234  			hashes = append(hashes, hash)
   235  		}
   236  		ret.Authentication = NewPackageHashAuthentication(target, hashes)
   237  	}
   238  
   239  	return ret, nil
   240  }
   241  
   242  // ForDisplay returns a string description of the source for user-facing output.
   243  func (s *HTTPMirrorSource) ForDisplay(provider addrs.Provider) string {
   244  	return "provider mirror at " + s.baseURL.String()
   245  }
   246  
   247  // mirrorHost extracts the hostname portion of the configured base URL and
   248  // returns it as a svchost.Hostname, normalized in the usual ways.
   249  //
   250  // If the returned error is non-nil then the given hostname doesn't comply
   251  // with the IETF RFC 5891 section 5.3 and 5.4 validation rules, and thus cannot
   252  // be interpreted as a valid Terraform service host. The IDNA validation errors
   253  // are unfortunately usually not very user-friendly, but they are also
   254  // relatively rare because the IDNA normalization rules are quite tolerant.
   255  func (s *HTTPMirrorSource) mirrorHost() (svchost.Hostname, error) {
   256  	return svchostFromURL(s.baseURL)
   257  }
   258  
   259  // mirrorHostCredentials returns the HostCredentials, if any, for the hostname
   260  // included in the mirror base URL.
   261  //
   262  // It might return an error if the mirror base URL is invalid, or if the
   263  // credentials lookup itself fails.
   264  func (s *HTTPMirrorSource) mirrorHostCredentials() (svcauth.HostCredentials, error) {
   265  	hostname, err := s.mirrorHost()
   266  	if err != nil {
   267  		return nil, fmt.Errorf("invalid provider mirror base URL %s: %s", s.baseURL.String(), err)
   268  	}
   269  
   270  	if s.creds == nil {
   271  		// No host-specific credentials, then.
   272  		return nil, nil
   273  	}
   274  
   275  	return s.creds.ForHost(hostname)
   276  }
   277  
   278  // get is the shared functionality for querying a JSON index from a mirror.
   279  //
   280  // It only handles the raw HTTP request. The "body" return value is the
   281  // reader from the response if and only if the response status code is 200 OK
   282  // and the Content-Type is application/json. In all other cases it's nil.
   283  // If body is non-nil then the caller must close it after reading it.
   284  //
   285  // If the "finalURL" return value is not empty then it's the URL that actually
   286  // produced the returned response, possibly after following some redirects.
   287  func (s *HTTPMirrorSource) get(ctx context.Context, relativePath string) (statusCode int, body io.ReadCloser, finalURL *url.URL, error error) {
   288  	endpointPath, err := url.Parse(relativePath)
   289  	if err != nil {
   290  		// Should never happen because the caller should validate all of the
   291  		// components it's including in the path.
   292  		return 0, nil, nil, err
   293  	}
   294  	endpointURL := s.baseURL.ResolveReference(endpointPath)
   295  
   296  	req, err := retryablehttp.NewRequest("GET", endpointURL.String(), nil)
   297  	if err != nil {
   298  		return 0, nil, endpointURL, err
   299  	}
   300  	req = req.WithContext(ctx)
   301  	req.Request.Header.Set(terraformVersionHeader, version.String())
   302  	creds, err := s.mirrorHostCredentials()
   303  	if err != nil {
   304  		return 0, nil, endpointURL, fmt.Errorf("failed to determine request credentials: %s", err)
   305  	}
   306  	if creds != nil {
   307  		// Note that if the initial requests gets redirected elsewhere
   308  		// then the credentials will still be included in the new request,
   309  		// even if they are on a different hostname. This is intentional
   310  		// and consistent with how we handle credentials for other
   311  		// Terraform-native services, because the user model is to configure
   312  		// credentials for the "friendly hostname" they configured, not for
   313  		// whatever hostname ends up ultimately serving the request as an
   314  		// implementation detail.
   315  		creds.PrepareRequest(req.Request)
   316  	}
   317  
   318  	resp, err := s.httpClient.Do(req)
   319  	if err != nil {
   320  		return 0, nil, endpointURL, err
   321  	}
   322  	defer func() {
   323  		// If we're not returning the body then we'll close it
   324  		// before we return.
   325  		if body == nil {
   326  			resp.Body.Close()
   327  		}
   328  	}()
   329  	// After this point, our final URL return value should always be the
   330  	// one from resp.Request, because that takes into account any redirects
   331  	// we followed along the way.
   332  	finalURL = resp.Request.URL
   333  
   334  	if resp.StatusCode == http.StatusOK {
   335  		// If and only if we get an OK response, we'll check that the response
   336  		// type is JSON and return the body reader.
   337  		ct := resp.Header.Get("Content-Type")
   338  		mt, params, err := mime.ParseMediaType(ct)
   339  		if err != nil {
   340  			return 0, nil, finalURL, fmt.Errorf("response has invalid Content-Type: %s", err)
   341  		}
   342  		if mt != "application/json" {
   343  			return 0, nil, finalURL, fmt.Errorf("response has invalid Content-Type: must be application/json")
   344  		}
   345  		for name := range params {
   346  			// The application/json content-type has no defined parameters,
   347  			// but some servers are configured to include a redundant "charset"
   348  			// parameter anyway, presumably out of a sense of completeness.
   349  			// We'll ignore them but warn that we're ignoring them in case the
   350  			// subsequent parsing fails due to the server trying to use an
   351  			// unsupported character encoding. (RFC 7159 defines its own
   352  			// JSON-specific character encoding rules.)
   353  			log.Printf("[WARN] Network mirror returned %q as part of its JSON content type, which is not defined. Ignoring.", name)
   354  		}
   355  		body = resp.Body
   356  	}
   357  
   358  	return resp.StatusCode, body, finalURL, nil
   359  }
   360  
   361  func (s *HTTPMirrorSource) errQueryFailed(provider addrs.Provider, err error) error {
   362  	if err == context.Canceled {
   363  		// This one has a special error type so that callers can
   364  		// handle it in a different way.
   365  		return ErrRequestCanceled{}
   366  	}
   367  	return ErrQueryFailed{
   368  		Provider:  provider,
   369  		Wrapped:   err,
   370  		MirrorURL: s.baseURL,
   371  	}
   372  }
   373  
   374  func (s *HTTPMirrorSource) errUnauthorized(finalURL *url.URL) error {
   375  	hostname, err := svchostFromURL(finalURL)
   376  	if err != nil {
   377  		// Again, weird but we'll tolerate it.
   378  		return fmt.Errorf("invalid credentials for %s", finalURL)
   379  	}
   380  
   381  	return ErrUnauthorized{
   382  		Hostname: hostname,
   383  
   384  		// We can't easily tell from here whether we had credentials or
   385  		// not, so for now we'll just assume we did because "host rejected
   386  		// the given credentials" is, hopefully, still understandable in
   387  		// the event that there were none. (If this ends up being confusing
   388  		// in practice then we'll need to do some refactoring of how
   389  		// we handle credentials in this source.)
   390  		HaveCredentials: true,
   391  	}
   392  }
   393  
   394  func svchostFromURL(u *url.URL) (svchost.Hostname, error) {
   395  	raw := u.Host
   396  
   397  	// When "friendly hostnames" appear in Terraform-specific identifiers we
   398  	// typically constrain their syntax more strictly than the
   399  	// Internationalized Domain Name specifications call for, such as
   400  	// forbidding direct use of punycode, but in this case we're just
   401  	// working with a standard http: or https: URL and so we'll first use the
   402  	// IDNA "lookup" rules directly, with no additional notational constraints,
   403  	// to effectively normalize away the differences that would normally
   404  	// produce an error.
   405  	var portPortion string
   406  	if colonPos := strings.Index(raw, ":"); colonPos != -1 {
   407  		raw, portPortion = raw[:colonPos], raw[colonPos:]
   408  	}
   409  	// HTTPMirrorSource requires all URLs to be https URLs, because running
   410  	// a network mirror over HTTP would potentially transmit any configured
   411  	// credentials in cleartext. Therefore we don't need to do any special
   412  	// handling of default ports here, because svchost.Hostname already
   413  	// considers the absense of a port to represent the standard HTTPS port
   414  	// 443, and will normalize away an explicit specification of port 443
   415  	// in svchost.ForComparison below.
   416  
   417  	normalized, err := idna.Display.ToUnicode(raw)
   418  	if err != nil {
   419  		return svchost.Hostname(""), err
   420  	}
   421  
   422  	// If ToUnicode succeeded above then "normalized" is now a hostname in the
   423  	// normalized IDNA form, with any direct punycode already interpreted and
   424  	// the case folding and other normalization rules applied. It should
   425  	// therefore now be accepted by svchost.ForComparison with no additional
   426  	// errors, but the port portion can still potentially be invalid.
   427  	return svchost.ForComparison(normalized + portPortion)
   428  }