1
2
3
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
53 url string
54
55
56
57
58
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
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
93
94
95 break
96 }
97
98
99
100
101 if strings.ContainsAny(url, ".:/") && !strings.Contains(url, ":/") && !filepath.IsAbs(url) && !path.IsAbs(url) {
102 url = "https://" + url
103 }
104
105
106
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
121
122
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
131
132
133
134
135
136
137
138
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
149
150
151
152
153
154
155
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 redactedBase string
191
192 listLatestOnce sync.Once
193 listLatest *RevInfo
194 listLatestErr error
195 }
196
197 func newProxyRepo(baseURL, path string) (Repo, error) {
198
199 base, err := url.Parse(baseURL)
200 if err != nil {
201 return nil, err
202 }
203 redactedBase := base.Redacted()
204 switch base.Scheme {
205 case "http", "https":
206
207 case "file":
208 if *base != (url.URL{Scheme: base.Scheme, Path: base.Path, RawPath: base.RawPath}) {
209 return nil, fmt.Errorf("invalid file:// proxy URL with non-path elements: %s", redactedBase)
210 }
211 case "":
212 return nil, fmt.Errorf("invalid proxy URL missing scheme: %s", redactedBase)
213 default:
214 return nil, fmt.Errorf("invalid proxy URL scheme (must be https, http, file): %s", redactedBase)
215 }
216
217
218 url := base
219 enc, err := module.EscapePath(path)
220 if err != nil {
221 return nil, err
222 }
223 url.Path = strings.TrimSuffix(base.Path, "/") + "/" + enc
224 url.RawPath = strings.TrimSuffix(base.RawPath, "/") + "/" + pathEscape(enc)
225
226 return &proxyRepo{url, path, redactedBase, sync.Once{}, nil, nil}, nil
227 }
228
229 func (p *proxyRepo) ModulePath() string {
230 return p.path
231 }
232
233 var errProxyReuse = fmt.Errorf("proxy does not support CheckReuse")
234
235 func (p *proxyRepo) CheckReuse(ctx context.Context, old *codehost.Origin) error {
236 return errProxyReuse
237 }
238
239
240 func (p *proxyRepo) versionError(version string, err error) error {
241 if version != "" && version != module.CanonicalVersion(version) {
242 return &module.ModuleError{
243 Path: p.path,
244 Err: &module.InvalidVersionError{
245 Version: version,
246 Pseudo: module.IsPseudoVersion(version),
247 Err: err,
248 },
249 }
250 }
251
252 return &module.ModuleError{
253 Path: p.path,
254 Version: version,
255 Err: err,
256 }
257 }
258
259 func (p *proxyRepo) getBytes(ctx context.Context, path string) ([]byte, error) {
260 body, redactedURL, err := p.getBody(ctx, path)
261 if err != nil {
262 return nil, err
263 }
264 defer body.Close()
265
266 b, err := io.ReadAll(body)
267 if err != nil {
268
269
270 return b, &url.Error{Op: "read", URL: redactedURL, Err: err}
271 }
272 return b, nil
273 }
274
275 func (p *proxyRepo) getBody(ctx context.Context, path string) (r io.ReadCloser, redactedURL string, err error) {
276 fullPath := pathpkg.Join(p.url.Path, path)
277
278 target := *p.url
279 target.Path = fullPath
280 target.RawPath = pathpkg.Join(target.RawPath, pathEscape(path))
281
282 resp, err := web.Get(web.DefaultSecurity, &target)
283 if err != nil {
284 return nil, "", err
285 }
286 if err := resp.Err(); err != nil {
287 resp.Body.Close()
288 return nil, "", err
289 }
290 return resp.Body, resp.URL, nil
291 }
292
293 func (p *proxyRepo) Versions(ctx context.Context, prefix string) (*Versions, error) {
294 data, err := p.getBytes(ctx, "@v/list")
295 if err != nil {
296 p.listLatestOnce.Do(func() {
297 p.listLatest, p.listLatestErr = nil, p.versionError("", err)
298 })
299 return nil, p.versionError("", err)
300 }
301 var list []string
302 allLine := strings.Split(string(data), "\n")
303 for _, line := range allLine {
304 f := strings.Fields(line)
305 if len(f) >= 1 && semver.IsValid(f[0]) && strings.HasPrefix(f[0], prefix) && !module.IsPseudoVersion(f[0]) {
306 list = append(list, f[0])
307 }
308 }
309 p.listLatestOnce.Do(func() {
310 p.listLatest, p.listLatestErr = p.latestFromList(ctx, allLine)
311 })
312 semver.Sort(list)
313 return &Versions{List: list}, nil
314 }
315
316 func (p *proxyRepo) latest(ctx context.Context) (*RevInfo, error) {
317 p.listLatestOnce.Do(func() {
318 data, err := p.getBytes(ctx, "@v/list")
319 if err != nil {
320 p.listLatestErr = p.versionError("", err)
321 return
322 }
323 list := strings.Split(string(data), "\n")
324 p.listLatest, p.listLatestErr = p.latestFromList(ctx, list)
325 })
326 return p.listLatest, p.listLatestErr
327 }
328
329 func (p *proxyRepo) latestFromList(ctx context.Context, allLine []string) (*RevInfo, error) {
330 var (
331 bestTime time.Time
332 bestVersion string
333 )
334 for _, line := range allLine {
335 f := strings.Fields(line)
336 if len(f) >= 1 && semver.IsValid(f[0]) {
337
338
339 var (
340 ft time.Time
341 )
342 if len(f) >= 2 {
343 ft, _ = time.Parse(time.RFC3339, f[1])
344 } else if module.IsPseudoVersion(f[0]) {
345 ft, _ = module.PseudoVersionTime(f[0])
346 } else {
347
348
349
350 continue
351 }
352 if bestTime.Before(ft) {
353 bestTime = ft
354 bestVersion = f[0]
355 }
356 }
357 }
358 if bestVersion == "" {
359 return nil, p.versionError("", codehost.ErrNoCommits)
360 }
361
362
363 return p.Stat(ctx, bestVersion)
364 }
365
366 func (p *proxyRepo) Stat(ctx context.Context, rev string) (*RevInfo, error) {
367 encRev, err := module.EscapeVersion(rev)
368 if err != nil {
369 return nil, p.versionError(rev, err)
370 }
371 data, err := p.getBytes(ctx, "@v/"+encRev+".info")
372 if err != nil {
373 return nil, p.versionError(rev, err)
374 }
375 info := new(RevInfo)
376 if err := json.Unmarshal(data, info); err != nil {
377 return nil, p.versionError(rev, fmt.Errorf("invalid response from proxy %q: %w", p.redactedBase, err))
378 }
379 if info.Version != rev && rev == module.CanonicalVersion(rev) && module.Check(p.path, rev) == nil {
380
381
382
383 return nil, p.versionError(rev, fmt.Errorf("proxy returned info for version %s instead of requested version", info.Version))
384 }
385 return info, nil
386 }
387
388 func (p *proxyRepo) Latest(ctx context.Context) (*RevInfo, error) {
389 data, err := p.getBytes(ctx, "@latest")
390 if err != nil {
391 if !errors.Is(err, fs.ErrNotExist) {
392 return nil, p.versionError("", err)
393 }
394 return p.latest(ctx)
395 }
396 info := new(RevInfo)
397 if err := json.Unmarshal(data, info); err != nil {
398 return nil, p.versionError("", fmt.Errorf("invalid response from proxy %q: %w", p.redactedBase, err))
399 }
400 return info, nil
401 }
402
403 func (p *proxyRepo) GoMod(ctx context.Context, version string) ([]byte, error) {
404 if version != module.CanonicalVersion(version) {
405 return nil, p.versionError(version, fmt.Errorf("internal error: version passed to GoMod is not canonical"))
406 }
407
408 encVer, err := module.EscapeVersion(version)
409 if err != nil {
410 return nil, p.versionError(version, err)
411 }
412 data, err := p.getBytes(ctx, "@v/"+encVer+".mod")
413 if err != nil {
414 return nil, p.versionError(version, err)
415 }
416 return data, nil
417 }
418
419 func (p *proxyRepo) Zip(ctx context.Context, dst io.Writer, version string) error {
420 if version != module.CanonicalVersion(version) {
421 return p.versionError(version, fmt.Errorf("internal error: version passed to Zip is not canonical"))
422 }
423
424 encVer, err := module.EscapeVersion(version)
425 if err != nil {
426 return p.versionError(version, err)
427 }
428 path := "@v/" + encVer + ".zip"
429 body, redactedURL, err := p.getBody(ctx, path)
430 if err != nil {
431 return p.versionError(version, err)
432 }
433 defer body.Close()
434
435 lr := &io.LimitedReader{R: body, N: codehost.MaxZipFile + 1}
436 if _, err := io.Copy(dst, lr); err != nil {
437
438
439 err = &url.Error{Op: "read", URL: redactedURL, Err: err}
440 return p.versionError(version, err)
441 }
442 if lr.N <= 0 {
443 return p.versionError(version, fmt.Errorf("downloaded zip file too large"))
444 }
445 return nil
446 }
447
448
449
450
451 func pathEscape(s string) string {
452 return strings.ReplaceAll(url.PathEscape(s), "%2F", "/")
453 }
454
View as plain text