github.com/gagliardetto/golang-go@v0.0.0-20201020153340-53909ea70814/cmd/go/not-internal/modfetch/proxy.go (about)

     1  // Copyright 2018 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package modfetch
     6  
     7  import (
     8  	"encoding/json"
     9  	"errors"
    10  	"fmt"
    11  	"io"
    12  	"io/ioutil"
    13  	"net/url"
    14  	"os"
    15  	"path"
    16  	pathpkg "path"
    17  	"path/filepath"
    18  	"strings"
    19  	"sync"
    20  	"time"
    21  
    22  	"github.com/gagliardetto/golang-go/cmd/go/not-internal/base"
    23  	"github.com/gagliardetto/golang-go/cmd/go/not-internal/cfg"
    24  	"github.com/gagliardetto/golang-go/cmd/go/not-internal/modfetch/codehost"
    25  	"github.com/gagliardetto/golang-go/cmd/go/not-internal/web"
    26  
    27  	"golang.org/x/mod/module"
    28  	"golang.org/x/mod/semver"
    29  )
    30  
    31  var HelpGoproxy = &base.Command{
    32  	UsageLine: "goproxy",
    33  	Short:     "module proxy protocol",
    34  	Long: `
    35  A Go module proxy is any web server that can respond to GET requests for
    36  URLs of a specified form. The requests have no query parameters, so even
    37  a site serving from a fixed file system (including a file:/// URL)
    38  can be a module proxy.
    39  
    40  The GET requests sent to a Go module proxy are:
    41  
    42  GET $GOPROXY/<module>/@v/list returns a list of known versions of the given
    43  module, one per line.
    44  
    45  GET $GOPROXY/<module>/@v/<version>.info returns JSON-formatted metadata
    46  about that version of the given module.
    47  
    48  GET $GOPROXY/<module>/@v/<version>.mod returns the go.mod file
    49  for that version of the given module.
    50  
    51  GET $GOPROXY/<module>/@v/<version>.zip returns the zip archive
    52  for that version of the given module.
    53  
    54  GET $GOPROXY/<module>/@latest returns JSON-formatted metadata about the
    55  latest known version of the given module in the same format as
    56  <module>/@v/<version>.info. The latest version should be the version of
    57  the module the go command may use if <module>/@v/list is empty or no
    58  listed version is suitable. <module>/@latest is optional and may not
    59  be implemented by a module proxy.
    60  
    61  When resolving the latest version of a module, the go command will request
    62  <module>/@v/list, then, if no suitable versions are found, <module>/@latest.
    63  The go command prefers, in order: the semantically highest release version,
    64  the semantically highest pre-release version, and the chronologically
    65  most recent pseudo-version. In Go 1.12 and earlier, the go command considered
    66  pseudo-versions in <module>/@v/list to be pre-release versions, but this is
    67  no longer true since Go 1.13.
    68  
    69  To avoid problems when serving from case-sensitive file systems,
    70  the <module> and <version> elements are case-encoded, replacing every
    71  uppercase letter with an exclamation mark followed by the corresponding
    72  lower-case letter: github.com/Azure encodes as github.com/!azure.
    73  
    74  The JSON-formatted metadata about a given module corresponds to
    75  this Go data structure, which may be expanded in the future:
    76  
    77      type Info struct {
    78          Version string    // version string
    79          Time    time.Time // commit time
    80      }
    81  
    82  The zip archive for a specific version of a given module is a
    83  standard zip file that contains the file tree corresponding
    84  to the module's source code and related files. The archive uses
    85  slash-separated paths, and every file path in the archive must
    86  begin with <module>@<version>/, where the module and version are
    87  substituted directly, not case-encoded. The root of the module
    88  file tree corresponds to the <module>@<version>/ prefix in the
    89  archive.
    90  
    91  Even when downloading directly from version control systems,
    92  the go command synthesizes explicit info, mod, and zip files
    93  and stores them in its local cache, $GOPATH/pkg/mod/cache/download,
    94  the same as if it had downloaded them directly from a proxy.
    95  The cache layout is the same as the proxy URL space, so
    96  serving $GOPATH/pkg/mod/cache/download at (or copying it to)
    97  https://example.com/proxy would let other users access those
    98  cached module versions with GOPROXY=https://example.com/proxy.
    99  `,
   100  }
   101  
   102  var proxyOnce struct {
   103  	sync.Once
   104  	list []string
   105  	err  error
   106  }
   107  
   108  func proxyURLs() ([]string, error) {
   109  	proxyOnce.Do(func() {
   110  		if cfg.GONOPROXY != "" && cfg.GOPROXY != "direct" {
   111  			proxyOnce.list = append(proxyOnce.list, "noproxy")
   112  		}
   113  		for _, proxyURL := range strings.Split(cfg.GOPROXY, ",") {
   114  			proxyURL = strings.TrimSpace(proxyURL)
   115  			if proxyURL == "" {
   116  				continue
   117  			}
   118  			if proxyURL == "off" {
   119  				// "off" always fails hard, so can stop walking list.
   120  				proxyOnce.list = append(proxyOnce.list, "off")
   121  				break
   122  			}
   123  			if proxyURL == "direct" {
   124  				proxyOnce.list = append(proxyOnce.list, "direct")
   125  				// For now, "direct" is the end of the line. We may decide to add some
   126  				// sort of fallback behavior for them in the future, so ignore
   127  				// subsequent entries for forward-compatibility.
   128  				break
   129  			}
   130  
   131  			// Single-word tokens are reserved for built-in behaviors, and anything
   132  			// containing the string ":/" or matching an absolute file path must be a
   133  			// complete URL. For all other paths, implicitly add "https://".
   134  			if strings.ContainsAny(proxyURL, ".:/") && !strings.Contains(proxyURL, ":/") && !filepath.IsAbs(proxyURL) && !path.IsAbs(proxyURL) {
   135  				proxyURL = "https://" + proxyURL
   136  			}
   137  
   138  			// Check that newProxyRepo accepts the URL.
   139  			// It won't do anything with the path.
   140  			_, err := newProxyRepo(proxyURL, "golang.org/x/text")
   141  			if err != nil {
   142  				proxyOnce.err = err
   143  				return
   144  			}
   145  			proxyOnce.list = append(proxyOnce.list, proxyURL)
   146  		}
   147  	})
   148  
   149  	return proxyOnce.list, proxyOnce.err
   150  }
   151  
   152  // TryProxies iterates f over each configured proxy (including "noproxy" and
   153  // "direct" if applicable) until f returns an error that is not
   154  // equivalent to os.ErrNotExist.
   155  //
   156  // TryProxies then returns that final error.
   157  //
   158  // If GOPROXY is set to "off", TryProxies invokes f once with the argument
   159  // "off".
   160  func TryProxies(f func(proxy string) error) error {
   161  	proxies, err := proxyURLs()
   162  	if err != nil {
   163  		return err
   164  	}
   165  	if len(proxies) == 0 {
   166  		return f("off")
   167  	}
   168  
   169  	var lastAttemptErr error
   170  	for _, proxy := range proxies {
   171  		err = f(proxy)
   172  		if !errors.Is(err, os.ErrNotExist) {
   173  			lastAttemptErr = err
   174  			break
   175  		}
   176  
   177  		// The error indicates that the module does not exist.
   178  		// In general we prefer to report the last such error,
   179  		// because it indicates the error that occurs after all other
   180  		// options have been exhausted.
   181  		//
   182  		// However, for modules in the NOPROXY list, the most useful error occurs
   183  		// first (with proxy set to "noproxy"), and the subsequent errors are all
   184  		// errNoProxy (which is not particularly helpful). Do not overwrite a more
   185  		// useful error with errNoproxy.
   186  		if lastAttemptErr == nil || !errors.Is(err, errNoproxy) {
   187  			lastAttemptErr = err
   188  		}
   189  	}
   190  	return lastAttemptErr
   191  }
   192  
   193  type proxyRepo struct {
   194  	url  *url.URL
   195  	path string
   196  }
   197  
   198  func newProxyRepo(baseURL, path string) (Repo, error) {
   199  	base, err := url.Parse(baseURL)
   200  	if err != nil {
   201  		return nil, err
   202  	}
   203  	switch base.Scheme {
   204  	case "http", "https":
   205  		// ok
   206  	case "file":
   207  		if *base != (url.URL{Scheme: base.Scheme, Path: base.Path, RawPath: base.RawPath}) {
   208  			return nil, fmt.Errorf("invalid file:// proxy URL with non-path elements: %s", web.Redacted(base))
   209  		}
   210  	case "":
   211  		return nil, fmt.Errorf("invalid proxy URL missing scheme: %s", web.Redacted(base))
   212  	default:
   213  		return nil, fmt.Errorf("invalid proxy URL scheme (must be https, http, file): %s", web.Redacted(base))
   214  	}
   215  
   216  	enc, err := module.EscapePath(path)
   217  	if err != nil {
   218  		return nil, err
   219  	}
   220  
   221  	base.Path = strings.TrimSuffix(base.Path, "/") + "/" + enc
   222  	base.RawPath = strings.TrimSuffix(base.RawPath, "/") + "/" + pathEscape(enc)
   223  	return &proxyRepo{base, path}, nil
   224  }
   225  
   226  func (p *proxyRepo) ModulePath() string {
   227  	return p.path
   228  }
   229  
   230  // versionError returns err wrapped in a ModuleError for p.path.
   231  func (p *proxyRepo) versionError(version string, err error) error {
   232  	if version != "" && version != module.CanonicalVersion(version) {
   233  		return &module.ModuleError{
   234  			Path: p.path,
   235  			Err: &module.InvalidVersionError{
   236  				Version: version,
   237  				Pseudo:  IsPseudoVersion(version),
   238  				Err:     err,
   239  			},
   240  		}
   241  	}
   242  
   243  	return &module.ModuleError{
   244  		Path:    p.path,
   245  		Version: version,
   246  		Err:     err,
   247  	}
   248  }
   249  
   250  func (p *proxyRepo) getBytes(path string) ([]byte, error) {
   251  	body, err := p.getBody(path)
   252  	if err != nil {
   253  		return nil, err
   254  	}
   255  	defer body.Close()
   256  	return ioutil.ReadAll(body)
   257  }
   258  
   259  func (p *proxyRepo) getBody(path string) (io.ReadCloser, error) {
   260  	fullPath := pathpkg.Join(p.url.Path, path)
   261  
   262  	target := *p.url
   263  	target.Path = fullPath
   264  	target.RawPath = pathpkg.Join(target.RawPath, pathEscape(path))
   265  
   266  	resp, err := web.Get(web.DefaultSecurity, &target)
   267  	if err != nil {
   268  		return nil, err
   269  	}
   270  	if err := resp.Err(); err != nil {
   271  		resp.Body.Close()
   272  		return nil, err
   273  	}
   274  	return resp.Body, nil
   275  }
   276  
   277  func (p *proxyRepo) Versions(prefix string) ([]string, error) {
   278  	data, err := p.getBytes("@v/list")
   279  	if err != nil {
   280  		return nil, p.versionError("", err)
   281  	}
   282  	var list []string
   283  	for _, line := range strings.Split(string(data), "\n") {
   284  		f := strings.Fields(line)
   285  		if len(f) >= 1 && semver.IsValid(f[0]) && strings.HasPrefix(f[0], prefix) && !IsPseudoVersion(f[0]) {
   286  			list = append(list, f[0])
   287  		}
   288  	}
   289  	SortVersions(list)
   290  	return list, nil
   291  }
   292  
   293  func (p *proxyRepo) latest() (*RevInfo, error) {
   294  	data, err := p.getBytes("@v/list")
   295  	if err != nil {
   296  		return nil, p.versionError("", err)
   297  	}
   298  
   299  	var (
   300  		bestTime             time.Time
   301  		bestTimeIsFromPseudo bool
   302  		bestVersion          string
   303  	)
   304  
   305  	for _, line := range strings.Split(string(data), "\n") {
   306  		f := strings.Fields(line)
   307  		if len(f) >= 1 && semver.IsValid(f[0]) {
   308  			// If the proxy includes timestamps, prefer the timestamp it reports.
   309  			// Otherwise, derive the timestamp from the pseudo-version.
   310  			var (
   311  				ft             time.Time
   312  				ftIsFromPseudo = false
   313  			)
   314  			if len(f) >= 2 {
   315  				ft, _ = time.Parse(time.RFC3339, f[1])
   316  			} else if IsPseudoVersion(f[0]) {
   317  				ft, _ = PseudoVersionTime(f[0])
   318  				ftIsFromPseudo = true
   319  			} else {
   320  				// Repo.Latest promises that this method is only called where there are
   321  				// no tagged versions. Ignore any tagged versions that were added in the
   322  				// meantime.
   323  				continue
   324  			}
   325  			if bestTime.Before(ft) {
   326  				bestTime = ft
   327  				bestTimeIsFromPseudo = ftIsFromPseudo
   328  				bestVersion = f[0]
   329  			}
   330  		}
   331  	}
   332  	if bestVersion == "" {
   333  		return nil, p.versionError("", codehost.ErrNoCommits)
   334  	}
   335  
   336  	if bestTimeIsFromPseudo {
   337  		// We parsed bestTime from the pseudo-version, but that's in UTC and we're
   338  		// supposed to report the timestamp as reported by the VCS.
   339  		// Stat the selected version to canonicalize the timestamp.
   340  		//
   341  		// TODO(bcmills): Should we also stat other versions to ensure that we
   342  		// report the correct Name and Short for the revision?
   343  		return p.Stat(bestVersion)
   344  	}
   345  
   346  	return &RevInfo{
   347  		Version: bestVersion,
   348  		Name:    bestVersion,
   349  		Short:   bestVersion,
   350  		Time:    bestTime,
   351  	}, nil
   352  }
   353  
   354  func (p *proxyRepo) Stat(rev string) (*RevInfo, error) {
   355  	encRev, err := module.EscapeVersion(rev)
   356  	if err != nil {
   357  		return nil, p.versionError(rev, err)
   358  	}
   359  	data, err := p.getBytes("@v/" + encRev + ".info")
   360  	if err != nil {
   361  		return nil, p.versionError(rev, err)
   362  	}
   363  	info := new(RevInfo)
   364  	if err := json.Unmarshal(data, info); err != nil {
   365  		return nil, p.versionError(rev, err)
   366  	}
   367  	if info.Version != rev && rev == module.CanonicalVersion(rev) && module.Check(p.path, rev) == nil {
   368  		// If we request a correct, appropriate version for the module path, the
   369  		// proxy must return either exactly that version or an error — not some
   370  		// arbitrary other version.
   371  		return nil, p.versionError(rev, fmt.Errorf("proxy returned info for version %s instead of requested version", info.Version))
   372  	}
   373  	return info, nil
   374  }
   375  
   376  func (p *proxyRepo) Latest() (*RevInfo, error) {
   377  	data, err := p.getBytes("@latest")
   378  	if err != nil {
   379  		if !errors.Is(err, os.ErrNotExist) {
   380  			return nil, p.versionError("", err)
   381  		}
   382  		return p.latest()
   383  	}
   384  	info := new(RevInfo)
   385  	if err := json.Unmarshal(data, info); err != nil {
   386  		return nil, p.versionError("", err)
   387  	}
   388  	return info, nil
   389  }
   390  
   391  func (p *proxyRepo) GoMod(version string) ([]byte, error) {
   392  	if version != module.CanonicalVersion(version) {
   393  		return nil, p.versionError(version, fmt.Errorf("internal error: version passed to GoMod is not canonical"))
   394  	}
   395  
   396  	encVer, err := module.EscapeVersion(version)
   397  	if err != nil {
   398  		return nil, p.versionError(version, err)
   399  	}
   400  	data, err := p.getBytes("@v/" + encVer + ".mod")
   401  	if err != nil {
   402  		return nil, p.versionError(version, err)
   403  	}
   404  	return data, nil
   405  }
   406  
   407  func (p *proxyRepo) Zip(dst io.Writer, version string) error {
   408  	if version != module.CanonicalVersion(version) {
   409  		return p.versionError(version, fmt.Errorf("internal error: version passed to Zip is not canonical"))
   410  	}
   411  
   412  	encVer, err := module.EscapeVersion(version)
   413  	if err != nil {
   414  		return p.versionError(version, err)
   415  	}
   416  	body, err := p.getBody("@v/" + encVer + ".zip")
   417  	if err != nil {
   418  		return p.versionError(version, err)
   419  	}
   420  	defer body.Close()
   421  
   422  	lr := &io.LimitedReader{R: body, N: codehost.MaxZipFile + 1}
   423  	if _, err := io.Copy(dst, lr); err != nil {
   424  		return p.versionError(version, err)
   425  	}
   426  	if lr.N <= 0 {
   427  		return p.versionError(version, fmt.Errorf("downloaded zip file too large"))
   428  	}
   429  	return nil
   430  }
   431  
   432  // pathEscape escapes s so it can be used in a path.
   433  // That is, it escapes things like ? and # (which really shouldn't appear anyway).
   434  // It does not escape / to %2F: our REST API is designed so that / can be left as is.
   435  func pathEscape(s string) string {
   436  	return strings.ReplaceAll(url.PathEscape(s), "%2F", "/")
   437  }