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.
22 "cmd/go/internal/base"
24 "cmd/go/internal/modfetch/codehost"
27 "golang.org/x/mod/module"
28 "golang.org/x/mod/semver"
31 var HelpGoproxy = &base.Command{
33 Short: "module proxy protocol",
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.
40 For details on the GOPROXY protocol, see
41 https://golang.org/ref/mod#goproxy-protocol.
45 var proxyOnce struct {
51 type proxySpec struct {
52 // url is the proxy URL or one of "off", "direct", "noproxy".
55 // fallBackOnError is true if a request should be attempted on the next proxy
56 // in the list after any error from this proxy. If fallBackOnError is false,
57 // the request will only be attempted on the next proxy if the error is
58 // equivalent to os.ErrNotFound, which is true for 404 and 410 responses.
62 func proxyList() ([]proxySpec, error) {
64 if cfg.GONOPROXY != "" && cfg.GOPROXY != "direct" {
65 proxyOnce.list = append(proxyOnce.list, proxySpec{url: "noproxy"})
68 goproxy := cfg.GOPROXY
71 fallBackOnError := false
72 if i := strings.IndexAny(goproxy, ",|"); i >= 0 {
74 fallBackOnError = goproxy[i] == '|'
75 goproxy = goproxy[i+1:]
81 url = strings.TrimSpace(url)
86 // "off" always fails hard, so can stop walking list.
87 proxyOnce.list = append(proxyOnce.list, proxySpec{url: "off"})
91 proxyOnce.list = append(proxyOnce.list, proxySpec{url: "direct"})
92 // For now, "direct" is the end of the line. We may decide to add some
93 // sort of fallback behavior for them in the future, so ignore
94 // subsequent entries for forward-compatibility.
98 // Single-word tokens are reserved for built-in behaviors, and anything
99 // containing the string ":/" or matching an absolute file path must be a
100 // complete URL. For all other paths, implicitly add "https://".
101 if strings.ContainsAny(url, ".:/") && !strings.Contains(url, ":/") && !filepath.IsAbs(url) && !path.IsAbs(url) {
102 url = "https://" + url
105 // Check that newProxyRepo accepts the URL.
106 // It won't do anything with the path.
107 if _, err := newProxyRepo(url, "golang.org/x/text"); err != nil {
112 proxyOnce.list = append(proxyOnce.list, proxySpec{
114 fallBackOnError: fallBackOnError,
118 if len(proxyOnce.list) == 0 ||
119 len(proxyOnce.list) == 1 && proxyOnce.list[0].url == "noproxy" {
120 // There were no proxies, other than the implicit "noproxy" added when
121 // GONOPROXY is set. This can happen if GOPROXY is a non-empty string
123 proxyOnce.err = fmt.Errorf("GOPROXY list is not the empty string, but contains no entries")
127 return proxyOnce.list, proxyOnce.err
130 // TryProxies iterates f over each configured proxy (including "noproxy" and
131 // "direct" if applicable) until f returns no error or until f returns an
132 // error that is not equivalent to fs.ErrNotExist on a proxy configured
133 // not to fall back on errors.
135 // TryProxies then returns that final error.
137 // If GOPROXY is set to "off", TryProxies invokes f once with the argument
139 func TryProxies(f func(proxy string) error) error {
140 proxies, err := proxyList()
144 if len(proxies) == 0 {
145 panic("GOPROXY list is empty")
148 // We try to report the most helpful error to the user. "direct" and "noproxy"
149 // errors are best, followed by proxy errors other than ErrNotExist, followed
152 // Note that errProxyOff, errNoproxy, and errUseProxy are equivalent to
153 // ErrNotExist. errUseProxy should only be returned if "noproxy" is the only
154 // proxy. errNoproxy should never be returned, since there should always be a
155 // more useful error from "noproxy" first.
162 bestErrRank := notExistRank
163 for _, proxy := range proxies {
168 isNotExistErr := errors.Is(err, fs.ErrNotExist)
170 if proxy.url == "direct" || (proxy.url == "noproxy" && err != errUseProxy) {
172 bestErrRank = directRank
173 } else if bestErrRank <= proxyRank && !isNotExistErr {
175 bestErrRank = proxyRank
176 } else if bestErrRank == notExistRank {
180 if !proxy.fallBackOnError && !isNotExistErr {
187 type proxyRepo struct {
192 listLatestOnce sync.Once
197 func newProxyRepo(baseURL, path string) (Repo, error) {
198 base, err := url.Parse(baseURL)
203 case "http", "https":
206 if *base != (url.URL{Scheme: base.Scheme, Path: base.Path, RawPath: base.RawPath}) {
207 return nil, fmt.Errorf("invalid file:// proxy URL with non-path elements: %s", base.Redacted())
210 return nil, fmt.Errorf("invalid proxy URL missing scheme: %s", base.Redacted())
212 return nil, fmt.Errorf("invalid proxy URL scheme (must be https, http, file): %s", base.Redacted())
215 enc, err := module.EscapePath(path)
219 redactedURL := base.Redacted()
220 base.Path = strings.TrimSuffix(base.Path, "/") + "/" + enc
221 base.RawPath = strings.TrimSuffix(base.RawPath, "/") + "/" + pathEscape(enc)
222 return &proxyRepo{base, path, redactedURL, sync.Once{}, nil, nil}, nil
225 func (p *proxyRepo) ModulePath() string {
229 var errProxyReuse = fmt.Errorf("proxy does not support CheckReuse")
231 func (p *proxyRepo) CheckReuse(ctx context.Context, old *codehost.Origin) error {
235 // versionError returns err wrapped in a ModuleError for p.path.
236 func (p *proxyRepo) versionError(version string, err error) error {
237 if version != "" && version != module.CanonicalVersion(version) {
238 return &module.ModuleError{
240 Err: &module.InvalidVersionError{
242 Pseudo: module.IsPseudoVersion(version),
248 return &module.ModuleError{
255 func (p *proxyRepo) getBytes(ctx context.Context, path string) ([]byte, error) {
256 body, err := p.getBody(ctx, path)
262 b, err := io.ReadAll(body)
264 // net/http doesn't add context to Body errors, so add it here.
265 // (See https://go.dev/issue/52727.)
266 return b, &url.Error{Op: "read", URL: strings.TrimSuffix(p.redactedURL, "/") + "/" + path, Err: err}
271 func (p *proxyRepo) getBody(ctx context.Context, path string) (r io.ReadCloser, err error) {
272 fullPath := pathpkg.Join(p.url.Path, path)
275 target.Path = fullPath
276 target.RawPath = pathpkg.Join(target.RawPath, pathEscape(path))
278 resp, err := web.Get(web.DefaultSecurity, &target)
282 if err := resp.Err(); err != nil {
286 return resp.Body, nil
289 func (p *proxyRepo) Versions(ctx context.Context, prefix string) (*Versions, error) {
290 data, err := p.getBytes(ctx, "@v/list")
292 p.listLatestOnce.Do(func() {
293 p.listLatest, p.listLatestErr = nil, p.versionError("", err)
295 return nil, p.versionError("", err)
298 allLine := strings.Split(string(data), "\n")
299 for _, line := range allLine {
300 f := strings.Fields(line)
301 if len(f) >= 1 && semver.IsValid(f[0]) && strings.HasPrefix(f[0], prefix) && !module.IsPseudoVersion(f[0]) {
302 list = append(list, f[0])
305 p.listLatestOnce.Do(func() {
306 p.listLatest, p.listLatestErr = p.latestFromList(ctx, allLine)
309 return &Versions{List: list}, nil
312 func (p *proxyRepo) latest(ctx context.Context) (*RevInfo, error) {
313 p.listLatestOnce.Do(func() {
314 data, err := p.getBytes(ctx, "@v/list")
316 p.listLatestErr = p.versionError("", err)
319 list := strings.Split(string(data), "\n")
320 p.listLatest, p.listLatestErr = p.latestFromList(ctx, list)
322 return p.listLatest, p.listLatestErr
325 func (p *proxyRepo) latestFromList(ctx context.Context, allLine []string) (*RevInfo, error) {
330 for _, line := range allLine {
331 f := strings.Fields(line)
332 if len(f) >= 1 && semver.IsValid(f[0]) {
333 // If the proxy includes timestamps, prefer the timestamp it reports.
334 // Otherwise, derive the timestamp from the pseudo-version.
339 ft, _ = time.Parse(time.RFC3339, f[1])
340 } else if module.IsPseudoVersion(f[0]) {
341 ft, _ = module.PseudoVersionTime(f[0])
343 // Repo.Latest promises that this method is only called where there are
344 // no tagged versions. Ignore any tagged versions that were added in the
348 if bestTime.Before(ft) {
354 if bestVersion == "" {
355 return nil, p.versionError("", codehost.ErrNoCommits)
358 // Call Stat to get all the other fields, including Origin information.
359 return p.Stat(ctx, bestVersion)
362 func (p *proxyRepo) Stat(ctx context.Context, rev string) (*RevInfo, error) {
363 encRev, err := module.EscapeVersion(rev)
365 return nil, p.versionError(rev, err)
367 data, err := p.getBytes(ctx, "@v/"+encRev+".info")
369 return nil, p.versionError(rev, err)
372 if err := json.Unmarshal(data, info); err != nil {
373 return nil, p.versionError(rev, fmt.Errorf("invalid response from proxy %q: %w", p.redactedURL, err))
375 if info.Version != rev && rev == module.CanonicalVersion(rev) && module.Check(p.path, rev) == nil {
376 // If we request a correct, appropriate version for the module path, the
377 // proxy must return either exactly that version or an error — not some
378 // arbitrary other version.
379 return nil, p.versionError(rev, fmt.Errorf("proxy returned info for version %s instead of requested version", info.Version))
384 func (p *proxyRepo) Latest(ctx context.Context) (*RevInfo, error) {
385 data, err := p.getBytes(ctx, "@latest")
387 if !errors.Is(err, fs.ErrNotExist) {
388 return nil, p.versionError("", err)
393 if err := json.Unmarshal(data, info); err != nil {
394 return nil, p.versionError("", fmt.Errorf("invalid response from proxy %q: %w", p.redactedURL, err))
399 func (p *proxyRepo) GoMod(ctx context.Context, version string) ([]byte, error) {
400 if version != module.CanonicalVersion(version) {
401 return nil, p.versionError(version, fmt.Errorf("internal error: version passed to GoMod is not canonical"))
404 encVer, err := module.EscapeVersion(version)
406 return nil, p.versionError(version, err)
408 data, err := p.getBytes(ctx, "@v/"+encVer+".mod")
410 return nil, p.versionError(version, err)
415 func (p *proxyRepo) Zip(ctx context.Context, dst io.Writer, version string) error {
416 if version != module.CanonicalVersion(version) {
417 return p.versionError(version, fmt.Errorf("internal error: version passed to Zip is not canonical"))
420 encVer, err := module.EscapeVersion(version)
422 return p.versionError(version, err)
424 path := "@v/" + encVer + ".zip"
425 body, err := p.getBody(ctx, path)
427 return p.versionError(version, err)
431 lr := &io.LimitedReader{R: body, N: codehost.MaxZipFile + 1}
432 if _, err := io.Copy(dst, lr); err != nil {
433 // net/http doesn't add context to Body errors, so add it here.
434 // (See https://go.dev/issue/52727.)
435 err = &url.Error{Op: "read", URL: strings.TrimSuffix(p.redactedURL, "/") + "/" + path, Err: err}
436 return p.versionError(version, err)
439 return p.versionError(version, fmt.Errorf("downloaded zip file too large"))
444 // pathEscape escapes s so it can be used in a path.
445 // That is, it escapes things like ? and # (which really shouldn't appear anyway).
446 // It does not escape / to %2F: our REST API is designed so that / can be left as is.
447 func pathEscape(s string) string {
448 return strings.ReplaceAll(url.PathEscape(s), "%2F", "/")