@@ -6,14 +6,16 @@ package modfetch
6
6
7
7
import (
8
8
"encoding/json"
9
+ "errors"
9
10
"fmt"
10
11
"io"
11
12
"io/ioutil"
12
- urlpkg "net/url"
13
+ url "net/url"
13
14
"os"
14
15
pathpkg "path"
15
16
"path/filepath"
16
17
"strings"
18
+ "sync"
17
19
"time"
18
20
19
21
"cmd/go/internal/base"
@@ -33,8 +35,13 @@ directly, just as 'go get' always has. The GOPROXY environment variable allows
33
35
further control over the download source. If GOPROXY is unset, is the empty string,
34
36
or is the string "direct", downloads use the default direct connection to version
35
37
control systems. Setting GOPROXY to "off" disallows downloading modules from
36
- any source. Otherwise, GOPROXY is expected to be the URL of a module proxy,
37
- in which case the go command will fetch all modules from that proxy.
38
+ any source. Otherwise, GOPROXY is expected to be a comma-separated list of
39
+ the URLs of module proxies, in which case the go command will fetch modules
40
+ from those proxies. For each request, the go command tries each proxy in sequence,
41
+ only moving to the next if the current proxy returns a 404 or 410 HTTP response.
42
+ The string "direct" may appear in the proxy list, to cause a direct connection to
43
+ be attempted at that point in the search.
44
+
38
45
No matter the source of the modules, downloaded modules must match existing
39
46
entries in go.sum (see 'go help modules' for discussion of verification).
40
47
@@ -100,47 +107,96 @@ func SetProxy(url string) {
100
107
proxyURL = url
101
108
}
102
109
110
+ var proxyOnce struct {
111
+ sync.Once
112
+ list []string
113
+ err error
114
+ }
115
+
116
+ func proxyURLs () ([]string , error ) {
117
+ proxyOnce .Do (func () {
118
+ for _ , proxyURL := range strings .Split (proxyURL , "," ) {
119
+ if proxyURL == "" {
120
+ continue
121
+ }
122
+ if proxyURL == "direct" {
123
+ proxyOnce .list = append (proxyOnce .list , "direct" )
124
+ continue
125
+ }
126
+
127
+ // Check that newProxyRepo accepts the URL.
128
+ // It won't do anything with the path.
129
+ _ , err := newProxyRepo (proxyURL , "golang.org/x/text" )
130
+ if err != nil {
131
+ proxyOnce .err = err
132
+ return
133
+ }
134
+ proxyOnce .list = append (proxyOnce .list , proxyURL )
135
+ }
136
+ })
137
+
138
+ return proxyOnce .list , proxyOnce .err
139
+ }
140
+
103
141
func lookupProxy (path string ) (Repo , error ) {
104
- if strings .Contains (proxyURL , "," ) {
105
- return nil , fmt .Errorf ("invalid $GOPROXY setting: cannot have comma" )
106
- }
107
- r , err := newProxyRepo (proxyURL , path )
142
+ list , err := proxyURLs ()
108
143
if err != nil {
109
144
return nil , err
110
145
}
111
- return r , nil
146
+
147
+ var repos listRepo
148
+ for _ , u := range list {
149
+ var r Repo
150
+ if u == "direct" {
151
+ // lookupDirect does actual network traffic.
152
+ // Especially if GOPROXY="http://mainproxy,direct",
153
+ // avoid the network until we need it by using a lazyRepo wrapper.
154
+ r = & lazyRepo {setup : lookupDirect , path : path }
155
+ } else {
156
+ // The URL itself was checked in proxyURLs.
157
+ // The only possible error here is a bad path,
158
+ // so we can return it unconditionally.
159
+ r , err = newProxyRepo (u , path )
160
+ if err != nil {
161
+ return nil , err
162
+ }
163
+ }
164
+ repos = append (repos , r )
165
+ }
166
+ return repos , nil
112
167
}
113
168
114
169
type proxyRepo struct {
115
- url * urlpkg .URL
170
+ url * url .URL
116
171
path string
117
172
}
118
173
119
174
func newProxyRepo (baseURL , path string ) (Repo , error ) {
120
- url , err := urlpkg .Parse (baseURL )
175
+ base , err := url .Parse (baseURL )
121
176
if err != nil {
122
177
return nil , err
123
178
}
124
- switch url .Scheme {
179
+ switch base .Scheme {
180
+ case "http" , "https" :
181
+ // ok
125
182
case "file" :
126
- if * url != (urlpkg .URL {Scheme : url .Scheme , Path : url .Path , RawPath : url .RawPath }) {
127
- return nil , fmt .Errorf ("proxy URL %q uses file scheme with non-path elements" , web .Redacted (url ))
183
+ if * base != (url .URL {Scheme : base .Scheme , Path : base .Path , RawPath : base .RawPath }) {
184
+ return nil , fmt .Errorf ("invalid file:// proxy URL with non-path elements: %s " , web .Redacted (base ))
128
185
}
129
- case "http" , "https" :
130
186
case "" :
131
- return nil , fmt .Errorf ("proxy URL %q missing scheme" , web .Redacted (url ))
187
+ return nil , fmt .Errorf ("invalid proxy URL missing scheme: %s " , web .Redacted (base ))
132
188
default :
133
- return nil , fmt .Errorf ("unsupported proxy scheme %q " , url . Scheme )
189
+ return nil , fmt .Errorf ("invalid proxy URL scheme (must be https, http, file): %s " , web . Redacted ( base ) )
134
190
}
135
191
136
192
enc , err := module .EncodePath (path )
137
193
if err != nil {
138
194
return nil , err
139
195
}
140
196
141
- url .Path = strings .TrimSuffix (url .Path , "/" ) + "/" + enc
142
- url .RawPath = strings .TrimSuffix (url .RawPath , "/" ) + "/" + pathEscape (enc )
143
- return & proxyRepo {url , path }, nil
197
+ base .Path = strings .TrimSuffix (base .Path , "/" ) + "/" + enc
198
+ base .RawPath = strings .TrimSuffix (base .RawPath , "/" ) + "/" + pathEscape (enc )
199
+ return & proxyRepo {base , path }, nil
144
200
}
145
201
146
202
func (p * proxyRepo ) ModulePath () string {
@@ -159,24 +215,24 @@ func (p *proxyRepo) getBytes(path string) ([]byte, error) {
159
215
func (p * proxyRepo ) getBody (path string ) (io.ReadCloser , error ) {
160
216
fullPath := pathpkg .Join (p .url .Path , path )
161
217
if p .url .Scheme == "file" {
162
- rawPath , err := urlpkg .PathUnescape (fullPath )
218
+ rawPath , err := url .PathUnescape (fullPath )
163
219
if err != nil {
164
220
return nil , err
165
221
}
166
222
return os .Open (filepath .FromSlash (rawPath ))
167
223
}
168
224
169
- url := new (urlpkg.URL )
170
- * url = * p .url
171
- url .Path = fullPath
172
- url .RawPath = pathpkg .Join (url .RawPath , pathEscape (path ))
225
+ target := * p .url
226
+ target .Path = fullPath
227
+ target .RawPath = pathpkg .Join (target .RawPath , pathEscape (path ))
173
228
174
- resp , err := web .Get (web .DefaultSecurity , url )
229
+ resp , err := web .Get (web .DefaultSecurity , & target )
175
230
if err != nil {
176
231
return nil , err
177
232
}
178
- if resp .StatusCode != 200 {
179
- return nil , fmt .Errorf ("unexpected status (%s): %v" , web .Redacted (url ), resp .Status )
233
+ if err := resp .Err (); err != nil {
234
+ resp .Body .Close ()
235
+ return nil , err
180
236
}
181
237
return resp .Body , nil
182
238
}
@@ -292,5 +348,119 @@ func (p *proxyRepo) Zip(dst io.Writer, version string) error {
292
348
// That is, it escapes things like ? and # (which really shouldn't appear anyway).
293
349
// It does not escape / to %2F: our REST API is designed so that / can be left as is.
294
350
func pathEscape (s string ) string {
295
- return strings .ReplaceAll (urlpkg .PathEscape (s ), "%2F" , "/" )
351
+ return strings .ReplaceAll (url .PathEscape (s ), "%2F" , "/" )
352
+ }
353
+
354
+ // A lazyRepo is a lazily-initialized Repo,
355
+ // constructed on demand by calling setup.
356
+ type lazyRepo struct {
357
+ path string
358
+ setup func (string ) (Repo , error )
359
+ once sync.Once
360
+ repo Repo
361
+ err error
362
+ }
363
+
364
+ func (r * lazyRepo ) init () {
365
+ r .repo , r .err = r .setup (r .path )
366
+ }
367
+
368
+ func (r * lazyRepo ) ModulePath () string {
369
+ return r .path
370
+ }
371
+
372
+ func (r * lazyRepo ) Versions (prefix string ) ([]string , error ) {
373
+ if r .once .Do (r .init ); r .err != nil {
374
+ return nil , r .err
375
+ }
376
+ return r .repo .Versions (prefix )
377
+ }
378
+
379
+ func (r * lazyRepo ) Stat (rev string ) (* RevInfo , error ) {
380
+ if r .once .Do (r .init ); r .err != nil {
381
+ return nil , r .err
382
+ }
383
+ return r .repo .Stat (rev )
384
+ }
385
+
386
+ func (r * lazyRepo ) Latest () (* RevInfo , error ) {
387
+ if r .once .Do (r .init ); r .err != nil {
388
+ return nil , r .err
389
+ }
390
+ return r .repo .Latest ()
391
+ }
392
+
393
+ func (r * lazyRepo ) GoMod (version string ) ([]byte , error ) {
394
+ if r .once .Do (r .init ); r .err != nil {
395
+ return nil , r .err
396
+ }
397
+ return r .repo .GoMod (version )
398
+ }
399
+
400
+ func (r * lazyRepo ) Zip (dst io.Writer , version string ) error {
401
+ if r .once .Do (r .init ); r .err != nil {
402
+ return r .err
403
+ }
404
+ return r .repo .Zip (dst , version )
405
+ }
406
+
407
+ // A listRepo is a preference list of Repos.
408
+ // The list must be non-empty and all Repos
409
+ // must return the same result from ModulePath.
410
+ // For each method, the repos are tried in order
411
+ // until one succeeds or returns a non-ErrNotExist (non-404) error.
412
+ type listRepo []Repo
413
+
414
+ func (l listRepo ) ModulePath () string {
415
+ return l [0 ].ModulePath ()
416
+ }
417
+
418
+ func (l listRepo ) Versions (prefix string ) ([]string , error ) {
419
+ for i , r := range l {
420
+ v , err := r .Versions (prefix )
421
+ if i == len (l )- 1 || ! errors .Is (err , os .ErrNotExist ) {
422
+ return v , err
423
+ }
424
+ }
425
+ panic ("no repos" )
426
+ }
427
+
428
+ func (l listRepo ) Stat (rev string ) (* RevInfo , error ) {
429
+ for i , r := range l {
430
+ info , err := r .Stat (rev )
431
+ if i == len (l )- 1 || ! errors .Is (err , os .ErrNotExist ) {
432
+ return info , err
433
+ }
434
+ }
435
+ panic ("no repos" )
436
+ }
437
+
438
+ func (l listRepo ) Latest () (* RevInfo , error ) {
439
+ for i , r := range l {
440
+ info , err := r .Latest ()
441
+ if i == len (l )- 1 || ! errors .Is (err , os .ErrNotExist ) {
442
+ return info , err
443
+ }
444
+ }
445
+ panic ("no repos" )
446
+ }
447
+
448
+ func (l listRepo ) GoMod (version string ) ([]byte , error ) {
449
+ for i , r := range l {
450
+ data , err := r .GoMod (version )
451
+ if i == len (l )- 1 || ! errors .Is (err , os .ErrNotExist ) {
452
+ return data , err
453
+ }
454
+ }
455
+ panic ("no repos" )
456
+ }
457
+
458
+ func (l listRepo ) Zip (dst io.Writer , version string ) error {
459
+ for i , r := range l {
460
+ err := r .Zip (dst , version )
461
+ if i == len (l )- 1 || ! errors .Is (err , os .ErrNotExist ) {
462
+ return err
463
+ }
464
+ }
465
+ panic ("no repos" )
296
466
}
0 commit comments