Skip to content

Commit f30c564

Browse files
committed
cmd/go: add support for GOPROXY list
Following discussion on golang.org/issue/26334, this CL changes the GOPROXY environment setting to be a list of proxies, tried in sequence. The first successful or non-404/410 error is taken as authoritative. Otherwise the next proxy is tried, and so on. As in earlier releases, GOPROXY=direct means "connect directly", but now it can appear in a longer list as well. This will let companies run a proxy holding only their private modules and let users set GOPROXY=thatproxy,publicproxy or GOPROXY=thatproxy,direct to fall back to an alternate mechanism for fetching public modules. Fixes #26334. Change-Id: I642f0ae655ec307d9cdcad0830c0baac8670eb9c Reviewed-on: https://go-review.googlesource.com/c/go/+/173441 Run-TryBot: Russ Cox <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Jay Conrod <[email protected]>
1 parent 5fa14a3 commit f30c564

File tree

5 files changed

+249
-30
lines changed

5 files changed

+249
-30
lines changed

src/cmd/go/alldocs.go

+7-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/cmd/go/internal/modfetch/proxy.go

+198-28
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,16 @@ package modfetch
66

77
import (
88
"encoding/json"
9+
"errors"
910
"fmt"
1011
"io"
1112
"io/ioutil"
12-
urlpkg "net/url"
13+
url "net/url"
1314
"os"
1415
pathpkg "path"
1516
"path/filepath"
1617
"strings"
18+
"sync"
1719
"time"
1820

1921
"cmd/go/internal/base"
@@ -33,8 +35,13 @@ directly, just as 'go get' always has. The GOPROXY environment variable allows
3335
further control over the download source. If GOPROXY is unset, is the empty string,
3436
or is the string "direct", downloads use the default direct connection to version
3537
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+
3845
No matter the source of the modules, downloaded modules must match existing
3946
entries in go.sum (see 'go help modules' for discussion of verification).
4047
@@ -100,47 +107,96 @@ func SetProxy(url string) {
100107
proxyURL = url
101108
}
102109

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+
103141
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()
108143
if err != nil {
109144
return nil, err
110145
}
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
112167
}
113168

114169
type proxyRepo struct {
115-
url *urlpkg.URL
170+
url *url.URL
116171
path string
117172
}
118173

119174
func newProxyRepo(baseURL, path string) (Repo, error) {
120-
url, err := urlpkg.Parse(baseURL)
175+
base, err := url.Parse(baseURL)
121176
if err != nil {
122177
return nil, err
123178
}
124-
switch url.Scheme {
179+
switch base.Scheme {
180+
case "http", "https":
181+
// ok
125182
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))
128185
}
129-
case "http", "https":
130186
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))
132188
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))
134190
}
135191

136192
enc, err := module.EncodePath(path)
137193
if err != nil {
138194
return nil, err
139195
}
140196

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
144200
}
145201

146202
func (p *proxyRepo) ModulePath() string {
@@ -159,24 +215,24 @@ func (p *proxyRepo) getBytes(path string) ([]byte, error) {
159215
func (p *proxyRepo) getBody(path string) (io.ReadCloser, error) {
160216
fullPath := pathpkg.Join(p.url.Path, path)
161217
if p.url.Scheme == "file" {
162-
rawPath, err := urlpkg.PathUnescape(fullPath)
218+
rawPath, err := url.PathUnescape(fullPath)
163219
if err != nil {
164220
return nil, err
165221
}
166222
return os.Open(filepath.FromSlash(rawPath))
167223
}
168224

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))
173228

174-
resp, err := web.Get(web.DefaultSecurity, url)
229+
resp, err := web.Get(web.DefaultSecurity, &target)
175230
if err != nil {
176231
return nil, err
177232
}
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
180236
}
181237
return resp.Body, nil
182238
}
@@ -292,5 +348,119 @@ func (p *proxyRepo) Zip(dst io.Writer, version string) error {
292348
// That is, it escapes things like ? and # (which really shouldn't appear anyway).
293349
// It does not escape / to %2F: our REST API is designed so that / can be left as is.
294350
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")
296466
}

src/cmd/go/internal/modfetch/repo.go

+4
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,10 @@ func lookup(path string) (r Repo, err error) {
209209
return lookupProxy(path)
210210
}
211211

212+
return lookupDirect(path)
213+
}
214+
215+
func lookupDirect(path string) (Repo, error) {
212216
security := web.SecureOnly
213217
if get.Insecure {
214218
security = web.Insecure

src/cmd/go/proxy_test.go

+11
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"net/http"
1717
"os"
1818
"path/filepath"
19+
"strconv"
1920
"strings"
2021
"sync"
2122
"testing"
@@ -104,6 +105,16 @@ func proxyHandler(w http.ResponseWriter, r *http.Request) {
104105
return
105106
}
106107
path := strings.TrimPrefix(r.URL.Path, "/mod/")
108+
109+
// If asked for 404/abc, serve a 404.
110+
if j := strings.Index(path, "/"); j >= 0 {
111+
n, err := strconv.Atoi(path[:j])
112+
if err == nil && n >= 200 {
113+
w.WriteHeader(n)
114+
return
115+
}
116+
}
117+
107118
i := strings.Index(path, "/@v/")
108119
if i < 0 {
109120
http.NotFound(w, r)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
env GO111MODULE=on
2+
env proxy=$GOPROXY
3+
4+
# Proxy that can't serve should fail.
5+
env GOPROXY=$proxy/404
6+
! go get rsc.io/[email protected]
7+
stderr '404 Not Found'
8+
9+
# get should walk down the proxy list past 404 and 410 responses.
10+
env GOPROXY=$proxy/404,$proxy/410,$proxy
11+
go get rsc.io/[email protected]
12+
13+
# get should not walk past other 4xx errors.
14+
env GOPROXY=$proxy/403,$proxy
15+
! go get rsc.io/[email protected]
16+
stderr 'reading.*/403/rsc.io/.*: 403 Forbidden'
17+
18+
# get should not walk past non-4xx errors.
19+
env GOPROXY=$proxy/500,$proxy
20+
! go get rsc.io/[email protected]
21+
stderr 'reading.*/500/rsc.io/.*: 500 Internal Server Error'
22+
23+
# get should return the final 404/410 if that's all we have.
24+
env GOPROXY=$proxy/404,$proxy/410
25+
! go get rsc.io/[email protected]
26+
stderr 'reading.*/410/rsc.io/.*: 410 Gone'
27+
28+
-- go.mod --
29+
module x

0 commit comments

Comments
 (0)