Copyright 2019 The Go Authors. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.
Package proxy provides a client for interacting with a proxy.
package proxy

import (
	
	
	
	
	
	
	
	
	
	
	
	

	
	
	
	
	
)
A Client is used by the fetch service to communicate with a module proxy. It handles all methods defined by go help goproxy.
URL of the module proxy web server
Client used for HTTP requests. It is mutable for testing purposes.
Whether fetch should be disabled.
One-element zip cache, to avoid a double download. See TestFetchAndUpdateStateCacheZip in internal/worker/fetch_test.go. Not thread-safe; should be used by only a single request goroutine.
A VersionInfo contains metadata about a given version of a module.
Setting this header to true prevents the proxy from fetching uncached modules.
const disableFetchHeader = "Disable-Module-Fetch"
New constructs a *Client using the provided url, which is expected to be an absolute URI that can be directly passed to http.Get.
func ( string) ( *Client,  error) {
	defer derrors.WrapStack(&, "proxy.New(%q)", )
	return &Client{
		url:          strings.TrimRight(, "/"),
		httpClient:   &http.Client{Transport: &ochttp.Transport{}},
		disableFetch: false,
	}, nil
}
WithFetchDisabled returns a new client that sets the Disable-Module-Fetch header so that the proxy does not fetch a module it doesn't already know about.
func ( *Client) () *Client {
	 := *
	.disableFetch = true
	return &
}
FetchDisabled reports whether proxy fetch is disabled.
func ( *Client) () bool {
	return .disableFetch
}
WithZipCache returns a new client that caches the last zip it downloads (not thread-safely).
func ( *Client) () *Client {
	 := *
	.rememberLastZip = true
	.lastZipModulePath = ""
	.lastZipVersion = ""
	.lastZipReader = nil
	return &
}
Info makes a request to $GOPROXY/<module>/@v/<requestedVersion>.info and transforms that data into a *VersionInfo. If requestedVersion is internal.LatestVersion, it uses the proxy's @latest endpoint instead.
func ( *Client) ( context.Context, ,  string) ( *VersionInfo,  error) {
Don't report NotFetched, because it is the normal result of fetching an uncached module when fetch is disabled. Don't report timeouts, because they are relatively frequent and not actionable.
		 := derrors.Wrap
		if !errors.Is(, derrors.NotFetched) && !errors.Is(, derrors.ProxyTimedOut) {
			 = derrors.WrapAndReport
		}
		(&, "proxy.Client.Info(%q, %q)", , )
	}()
	,  := .readBody(, , , "info")
	if  != nil {
		return nil, 
	}
	var  VersionInfo
	if  := json.Unmarshal(, &);  != nil {
		return nil, 
	}
	return &, nil
}
Mod makes a request to $GOPROXY/<module>/@v/<resolvedVersion>.mod and returns the raw data.
func ( *Client) ( context.Context, ,  string) ( []byte,  error) {
	defer derrors.WrapStack(&, "proxy.Client.Mod(%q, %q)", , )
	return .readBody(, , , "mod")
}
Zip makes a request to $GOPROXY/<modulePath>/@v/<resolvedVersion>.zip and transforms that data into a *zip.Reader. <resolvedVersion> must have already been resolved by first making a request to $GOPROXY/<modulePath>/@v/<requestedVersion>.info to obtained the valid semantic version.
func ( *Client) ( context.Context, ,  string) ( *zip.Reader,  error) {
	defer derrors.WrapStack(&, "proxy.Client.Zip(ctx, %q, %q)", , )

	if .lastZipModulePath ==  && .lastZipVersion ==  {
		return .lastZipReader, nil
	}
	,  := .readBody(, , , "zip")
	if  != nil {
		return nil, 
	}
	,  := zip.NewReader(bytes.NewReader(), int64(len()))
	if  != nil {
		return nil, fmt.Errorf("zip.NewReader: %v: %w", , derrors.BadModule)
	}
	if .rememberLastZip {
		.lastZipModulePath = 
		.lastZipVersion = 
		.lastZipReader = 
	}
	return , nil
}
ZipSize gets the size in bytes of the zip from the proxy, without downloading it. The version must be resolved, as by a call to Client.Info.
func ( *Client) ( context.Context, ,  string) ( int64,  error) {
	defer derrors.WrapStack(&, "proxy.Client.ZipSize(ctx, %q, %q)", , )

	,  := .escapedURL(, , "zip")
	if  != nil {
		return 0, 
	}
	,  := ctxhttp.Head(, .httpClient, )
	if  != nil {
		return 0, fmt.Errorf("ctxhttp.Head(ctx, client, %q): %v", , )
	}
	defer .Body.Close()
	if  := responseError(, false);  != nil {
		return 0, 
	}
	if .ContentLength < 0 {
		return 0, errors.New("unknown content length")
	}
	return .ContentLength, nil
}

func ( *Client) (, ,  string) ( string,  error) {
	defer derrors.WrapStack(&, "Client.escapedURL(%q, %q, %q)", , , )

	if  != "info" &&  != "mod" &&  != "zip" {
		return "", errors.New(`suffix must be "info", "mod" or "zip"`)
	}
	,  := module.EscapePath()
	if  != nil {
		return "", fmt.Errorf("path: %v: %w", , derrors.InvalidArgument)
	}
	if  == internal.LatestVersion {
		if  != "info" {
			return "", fmt.Errorf("cannot ask for latest with suffix %q", )
		}
		return fmt.Sprintf("%s/%s/@latest", .url, ), nil
	}
	,  := module.EscapeVersion()
	if  != nil {
		return "", fmt.Errorf("version: %v: %w", , derrors.InvalidArgument)
	}
	return fmt.Sprintf("%s/%s/@v/%s.%s", .url, , , ), nil
}

func ( *Client) ( context.Context, , ,  string) ( []byte,  error) {
	defer derrors.WrapStack(&, "Client.readBody(%q, %q, %q)", , , )

	,  := .escapedURL(, , )
	if  != nil {
		return nil, 
	}
	var  []byte
	 = .executeRequest(, , func( io.Reader) error {
		var  error
		,  = ioutil.ReadAll()
		return 
	})
	if  != nil {
		return nil, 
	}
	return , nil
}
Versions makes a request to $GOPROXY/<path>/@v/list and returns the resulting version strings.
func ( *Client) ( context.Context,  string) ( []string,  error) {
	,  := module.EscapePath()
	if  != nil {
		return nil, fmt.Errorf("module.EscapePath(%q): %w", , derrors.InvalidArgument)
	}
	 := fmt.Sprintf("%s/%s/@v/list", .url, )
	var  []string
	 := func( io.Reader) error {
		 := bufio.NewScanner()
		for .Scan() {
			 = append(, .Text())
		}
		return .Err()
	}
	if  := .executeRequest(, , );  != nil {
		return nil, 
	}
	return , nil
}
executeRequest executes an HTTP GET request for u, then calls the bodyFunc on the response body, if no error occurred.
func ( *Client) ( context.Context,  string,  func( io.Reader) error) ( error) {
	defer func() {
		if .Err() != nil {
			 = fmt.Errorf("%v: %w", , derrors.ProxyTimedOut)
		}
		derrors.WrapStack(&, "executeRequest(ctx, %q)", )
	}()

	,  := http.NewRequest("GET", , nil)
	if  != nil {
		return 
	}
	if .disableFetch {
		.Header.Set(disableFetchHeader, "true")
	}
	,  := ctxhttp.Do(, .httpClient, )
	if  != nil {
		return fmt.Errorf("ctxhttp.Do(ctx, client, %q): %v", , )
	}
	defer .Body.Close()
	if  := responseError(, .disableFetch);  != nil {
		return 
	}
	return (.Body)
}
responseError translates the response status code to an appropriate error.
func ( *http.Response,  bool) error {
	switch {
	case 200 <= .StatusCode && .StatusCode < 300:
		return nil
	case 500 <= .StatusCode:
		return derrors.ProxyError
	case .StatusCode == http.StatusNotFound,
Treat both 404 Not Found and 410 Gone responses from the proxy as a "not found" error category. If the response body contains "fetch timed out", treat this as a 504 response so that we retry fetching the module version again later. If the Disable-Module-Fetch header was set, use a different error code so we can tell the difference.
		,  := ioutil.ReadAll(.Body)
		if  != nil {
			return fmt.Errorf("ioutil.readall: %v", )
		}
		 := string()
		switch {
		case strings.Contains(, "fetch timed out"):
			 = derrors.ProxyTimedOut
		case :
			 = derrors.NotFetched
		default:
			 = derrors.NotFound
		}
		return fmt.Errorf("%q: %w", , )
	default:
		return fmt.Errorf("unexpected status %d %s", .StatusCode, .Status)
	}