]> Cypherpunks.ru repositories - gostls13.git/blob - src/cmd/go/internal/modfetch/proxy.go
cmd/go/internal/modfetch: avoid path.Join in URL errors, part 2
[gostls13.git] / src / cmd / go / internal / modfetch / proxy.go
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         "context"
9         "encoding/json"
10         "errors"
11         "fmt"
12         "io"
13         "io/fs"
14         "net/url"
15         "path"
16         pathpkg "path"
17         "path/filepath"
18         "strings"
19         "sync"
20         "time"
21
22         "cmd/go/internal/base"
23         "cmd/go/internal/cfg"
24         "cmd/go/internal/modfetch/codehost"
25         "cmd/go/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 For details on the GOPROXY protocol, see
41 https://golang.org/ref/mod#goproxy-protocol.
42 `,
43 }
44
45 var proxyOnce struct {
46         sync.Once
47         list []proxySpec
48         err  error
49 }
50
51 type proxySpec struct {
52         // url is the proxy URL or one of "off", "direct", "noproxy".
53         url string
54
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.
59         fallBackOnError bool
60 }
61
62 func proxyList() ([]proxySpec, error) {
63         proxyOnce.Do(func() {
64                 if cfg.GONOPROXY != "" && cfg.GOPROXY != "direct" {
65                         proxyOnce.list = append(proxyOnce.list, proxySpec{url: "noproxy"})
66                 }
67
68                 goproxy := cfg.GOPROXY
69                 for goproxy != "" {
70                         var url string
71                         fallBackOnError := false
72                         if i := strings.IndexAny(goproxy, ",|"); i >= 0 {
73                                 url = goproxy[:i]
74                                 fallBackOnError = goproxy[i] == '|'
75                                 goproxy = goproxy[i+1:]
76                         } else {
77                                 url = goproxy
78                                 goproxy = ""
79                         }
80
81                         url = strings.TrimSpace(url)
82                         if url == "" {
83                                 continue
84                         }
85                         if url == "off" {
86                                 // "off" always fails hard, so can stop walking list.
87                                 proxyOnce.list = append(proxyOnce.list, proxySpec{url: "off"})
88                                 break
89                         }
90                         if url == "direct" {
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.
95                                 break
96                         }
97
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
103                         }
104
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 {
108                                 proxyOnce.err = err
109                                 return
110                         }
111
112                         proxyOnce.list = append(proxyOnce.list, proxySpec{
113                                 url:             url,
114                                 fallBackOnError: fallBackOnError,
115                         })
116                 }
117
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
122                         // like "," or " ".
123                         proxyOnce.err = fmt.Errorf("GOPROXY list is not the empty string, but contains no entries")
124                 }
125         })
126
127         return proxyOnce.list, proxyOnce.err
128 }
129
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.
134 //
135 // TryProxies then returns that final error.
136 //
137 // If GOPROXY is set to "off", TryProxies invokes f once with the argument
138 // "off".
139 func TryProxies(f func(proxy string) error) error {
140         proxies, err := proxyList()
141         if err != nil {
142                 return err
143         }
144         if len(proxies) == 0 {
145                 panic("GOPROXY list is empty")
146         }
147
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
150         // by ErrNotExist.
151         //
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.
156         const (
157                 notExistRank = iota
158                 proxyRank
159                 directRank
160         )
161         var bestErr error
162         bestErrRank := notExistRank
163         for _, proxy := range proxies {
164                 err := f(proxy.url)
165                 if err == nil {
166                         return nil
167                 }
168                 isNotExistErr := errors.Is(err, fs.ErrNotExist)
169
170                 if proxy.url == "direct" || (proxy.url == "noproxy" && err != errUseProxy) {
171                         bestErr = err
172                         bestErrRank = directRank
173                 } else if bestErrRank <= proxyRank && !isNotExistErr {
174                         bestErr = err
175                         bestErrRank = proxyRank
176                 } else if bestErrRank == notExistRank {
177                         bestErr = err
178                 }
179
180                 if !proxy.fallBackOnError && !isNotExistErr {
181                         break
182                 }
183         }
184         return bestErr
185 }
186
187 type proxyRepo struct {
188         url         *url.URL
189         path        string
190         redactedURL string
191
192         listLatestOnce sync.Once
193         listLatest     *RevInfo
194         listLatestErr  error
195 }
196
197 func newProxyRepo(baseURL, path string) (Repo, error) {
198         base, err := url.Parse(baseURL)
199         if err != nil {
200                 return nil, err
201         }
202         switch base.Scheme {
203         case "http", "https":
204                 // ok
205         case "file":
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())
208                 }
209         case "":
210                 return nil, fmt.Errorf("invalid proxy URL missing scheme: %s", base.Redacted())
211         default:
212                 return nil, fmt.Errorf("invalid proxy URL scheme (must be https, http, file): %s", base.Redacted())
213         }
214
215         enc, err := module.EscapePath(path)
216         if err != nil {
217                 return nil, err
218         }
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
223 }
224
225 func (p *proxyRepo) ModulePath() string {
226         return p.path
227 }
228
229 var errProxyReuse = fmt.Errorf("proxy does not support CheckReuse")
230
231 func (p *proxyRepo) CheckReuse(ctx context.Context, old *codehost.Origin) error {
232         return errProxyReuse
233 }
234
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{
239                         Path: p.path,
240                         Err: &module.InvalidVersionError{
241                                 Version: version,
242                                 Pseudo:  module.IsPseudoVersion(version),
243                                 Err:     err,
244                         },
245                 }
246         }
247
248         return &module.ModuleError{
249                 Path:    p.path,
250                 Version: version,
251                 Err:     err,
252         }
253 }
254
255 func (p *proxyRepo) getBytes(ctx context.Context, path string) ([]byte, error) {
256         body, err := p.getBody(ctx, path)
257         if err != nil {
258                 return nil, err
259         }
260         defer body.Close()
261
262         b, err := io.ReadAll(body)
263         if err != nil {
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}
267         }
268         return b, nil
269 }
270
271 func (p *proxyRepo) getBody(ctx context.Context, path string) (r io.ReadCloser, err error) {
272         fullPath := pathpkg.Join(p.url.Path, path)
273
274         target := *p.url
275         target.Path = fullPath
276         target.RawPath = pathpkg.Join(target.RawPath, pathEscape(path))
277
278         resp, err := web.Get(web.DefaultSecurity, &target)
279         if err != nil {
280                 return nil, err
281         }
282         if err := resp.Err(); err != nil {
283                 resp.Body.Close()
284                 return nil, err
285         }
286         return resp.Body, nil
287 }
288
289 func (p *proxyRepo) Versions(ctx context.Context, prefix string) (*Versions, error) {
290         data, err := p.getBytes(ctx, "@v/list")
291         if err != nil {
292                 p.listLatestOnce.Do(func() {
293                         p.listLatest, p.listLatestErr = nil, p.versionError("", err)
294                 })
295                 return nil, p.versionError("", err)
296         }
297         var list []string
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])
303                 }
304         }
305         p.listLatestOnce.Do(func() {
306                 p.listLatest, p.listLatestErr = p.latestFromList(ctx, allLine)
307         })
308         semver.Sort(list)
309         return &Versions{List: list}, nil
310 }
311
312 func (p *proxyRepo) latest(ctx context.Context) (*RevInfo, error) {
313         p.listLatestOnce.Do(func() {
314                 data, err := p.getBytes(ctx, "@v/list")
315                 if err != nil {
316                         p.listLatestErr = p.versionError("", err)
317                         return
318                 }
319                 list := strings.Split(string(data), "\n")
320                 p.listLatest, p.listLatestErr = p.latestFromList(ctx, list)
321         })
322         return p.listLatest, p.listLatestErr
323 }
324
325 func (p *proxyRepo) latestFromList(ctx context.Context, allLine []string) (*RevInfo, error) {
326         var (
327                 bestTime    time.Time
328                 bestVersion string
329         )
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.
335                         var (
336                                 ft time.Time
337                         )
338                         if len(f) >= 2 {
339                                 ft, _ = time.Parse(time.RFC3339, f[1])
340                         } else if module.IsPseudoVersion(f[0]) {
341                                 ft, _ = module.PseudoVersionTime(f[0])
342                         } else {
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
345                                 // meantime.
346                                 continue
347                         }
348                         if bestTime.Before(ft) {
349                                 bestTime = ft
350                                 bestVersion = f[0]
351                         }
352                 }
353         }
354         if bestVersion == "" {
355                 return nil, p.versionError("", codehost.ErrNoCommits)
356         }
357
358         // Call Stat to get all the other fields, including Origin information.
359         return p.Stat(ctx, bestVersion)
360 }
361
362 func (p *proxyRepo) Stat(ctx context.Context, rev string) (*RevInfo, error) {
363         encRev, err := module.EscapeVersion(rev)
364         if err != nil {
365                 return nil, p.versionError(rev, err)
366         }
367         data, err := p.getBytes(ctx, "@v/"+encRev+".info")
368         if err != nil {
369                 return nil, p.versionError(rev, err)
370         }
371         info := new(RevInfo)
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))
374         }
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))
380         }
381         return info, nil
382 }
383
384 func (p *proxyRepo) Latest(ctx context.Context) (*RevInfo, error) {
385         data, err := p.getBytes(ctx, "@latest")
386         if err != nil {
387                 if !errors.Is(err, fs.ErrNotExist) {
388                         return nil, p.versionError("", err)
389                 }
390                 return p.latest(ctx)
391         }
392         info := new(RevInfo)
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))
395         }
396         return info, nil
397 }
398
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"))
402         }
403
404         encVer, err := module.EscapeVersion(version)
405         if err != nil {
406                 return nil, p.versionError(version, err)
407         }
408         data, err := p.getBytes(ctx, "@v/"+encVer+".mod")
409         if err != nil {
410                 return nil, p.versionError(version, err)
411         }
412         return data, nil
413 }
414
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"))
418         }
419
420         encVer, err := module.EscapeVersion(version)
421         if err != nil {
422                 return p.versionError(version, err)
423         }
424         path := "@v/" + encVer + ".zip"
425         body, err := p.getBody(ctx, path)
426         if err != nil {
427                 return p.versionError(version, err)
428         }
429         defer body.Close()
430
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)
437         }
438         if lr.N <= 0 {
439                 return p.versionError(version, fmt.Errorf("downloaded zip file too large"))
440         }
441         return nil
442 }
443
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", "/")
449 }