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 The GET requests sent to a Go module proxy are:
42 GET $GOPROXY/<module>/@v/list returns a list of known versions of the given
45 GET $GOPROXY/<module>/@v/<version>.info returns JSON-formatted metadata
46 about that version of the given module.
48 GET $GOPROXY/<module>/@v/<version>.mod returns the go.mod file
49 for that version of the given module.
51 GET $GOPROXY/<module>/@v/<version>.zip returns the zip archive
52 for that version of the given module.
54 GET $GOPROXY/<module>/@latest returns JSON-formatted metadata about the
55 latest known version of the given module in the same format as
56 <module>/@v/<version>.info. The latest version should be the version of
57 the module the go command may use if <module>/@v/list is empty or no
58 listed version is suitable. <module>/@latest is optional and may not
59 be implemented by a module proxy.
61 When resolving the latest version of a module, the go command will request
62 <module>/@v/list, then, if no suitable versions are found, <module>/@latest.
63 The go command prefers, in order: the semantically highest release version,
64 the semantically highest pre-release version, and the chronologically
65 most recent pseudo-version. In Go 1.12 and earlier, the go command considered
66 pseudo-versions in <module>/@v/list to be pre-release versions, but this is
67 no longer true since Go 1.13.
69 To avoid problems when serving from case-sensitive file systems,
70 the <module> and <version> elements are case-encoded, replacing every
71 uppercase letter with an exclamation mark followed by the corresponding
72 lower-case letter: github.com/Azure encodes as github.com/!azure.
74 The JSON-formatted metadata about a given module corresponds to
75 this Go data structure, which may be expanded in the future:
78 Version string // version string
79 Time time.Time // commit time
82 The zip archive for a specific version of a given module is a
83 standard zip file that contains the file tree corresponding
84 to the module's source code and related files. The archive uses
85 slash-separated paths, and every file path in the archive must
86 begin with <module>@<version>/, where the module and version are
87 substituted directly, not case-encoded. The root of the module
88 file tree corresponds to the <module>@<version>/ prefix in the
91 Even when downloading directly from version control systems,
92 the go command synthesizes explicit info, mod, and zip files
93 and stores them in its local cache, $GOPATH/pkg/mod/cache/download,
94 the same as if it had downloaded them directly from a proxy.
95 The cache layout is the same as the proxy URL space, so
96 serving $GOPATH/pkg/mod/cache/download at (or copying it to)
97 https://example.com/proxy would let other users access those
98 cached module versions with GOPROXY=https://example.com/proxy.
102 var proxyOnce struct {
108 func proxyURLs() ([]string, error) {
109 proxyOnce.Do(func() {
110 if cfg.GONOPROXY != "" && cfg.GOPROXY != "direct" {
111 proxyOnce.list = append(proxyOnce.list, "noproxy")
113 for _, proxyURL := range strings.Split(cfg.GOPROXY, ",") {
114 proxyURL = strings.TrimSpace(proxyURL)
118 if proxyURL == "off" {
119 // "off" always fails hard, so can stop walking list.
120 proxyOnce.list = append(proxyOnce.list, "off")
123 if proxyURL == "direct" {
124 proxyOnce.list = append(proxyOnce.list, "direct")
125 // For now, "direct" is the end of the line. We may decide to add some
126 // sort of fallback behavior for them in the future, so ignore
127 // subsequent entries for forward-compatibility.
131 // Single-word tokens are reserved for built-in behaviors, and anything
132 // containing the string ":/" or matching an absolute file path must be a
133 // complete URL. For all other paths, implicitly add "https://".
134 if strings.ContainsAny(proxyURL, ".:/") && !strings.Contains(proxyURL, ":/") && !filepath.IsAbs(proxyURL) && !path.IsAbs(proxyURL) {
135 proxyURL = "https://" + proxyURL
138 // Check that newProxyRepo accepts the URL.
139 // It won't do anything with the path.
140 _, err := newProxyRepo(proxyURL, "golang.org/x/text")
145 proxyOnce.list = append(proxyOnce.list, proxyURL)
149 return proxyOnce.list, proxyOnce.err
152 // TryProxies iterates f over each configured proxy (including "noproxy" and
153 // "direct" if applicable) until f returns an error that is not
154 // equivalent to os.ErrNotExist.
156 // TryProxies then returns that final error.
158 // If GOPROXY is set to "off", TryProxies invokes f once with the argument
160 func TryProxies(f func(proxy string) error) error {
161 proxies, err := proxyURLs()
165 if len(proxies) == 0 {
169 var lastAttemptErr error
170 for _, proxy := range proxies {
172 if !errors.Is(err, os.ErrNotExist) {
177 // The error indicates that the module does not exist.
178 // In general we prefer to report the last such error,
179 // because it indicates the error that occurs after all other
180 // options have been exhausted.
182 // However, for modules in the NOPROXY list, the most useful error occurs
183 // first (with proxy set to "noproxy"), and the subsequent errors are all
184 // errNoProxy (which is not particularly helpful). Do not overwrite a more
185 // useful error with errNoproxy.
186 if lastAttemptErr == nil || !errors.Is(err, errNoproxy) {
190 return lastAttemptErr
193 type proxyRepo struct {
198 func newProxyRepo(baseURL, path string) (Repo, error) {
199 base, err := url.Parse(baseURL)
204 case "http", "https":
207 if *base != (url.URL{Scheme: base.Scheme, Path: base.Path, RawPath: base.RawPath}) {
208 return nil, fmt.Errorf("invalid file:// proxy URL with non-path elements: %s", web.Redacted(base))
211 return nil, fmt.Errorf("invalid proxy URL missing scheme: %s", web.Redacted(base))
213 return nil, fmt.Errorf("invalid proxy URL scheme (must be https, http, file): %s", web.Redacted(base))
216 enc, err := module.EscapePath(path)
221 base.Path = strings.TrimSuffix(base.Path, "/") + "/" + enc
222 base.RawPath = strings.TrimSuffix(base.RawPath, "/") + "/" + pathEscape(enc)
223 return &proxyRepo{base, path}, nil
226 func (p *proxyRepo) ModulePath() string {
230 // versionError returns err wrapped in a ModuleError for p.path.
231 func (p *proxyRepo) versionError(version string, err error) error {
232 if version != "" && version != module.CanonicalVersion(version) {
233 return &module.ModuleError{
235 Err: &module.InvalidVersionError{
237 Pseudo: IsPseudoVersion(version),
243 return &module.ModuleError{
250 func (p *proxyRepo) getBytes(path string) ([]byte, error) {
251 body, err := p.getBody(path)
256 return ioutil.ReadAll(body)
259 func (p *proxyRepo) getBody(path string) (io.ReadCloser, error) {
260 fullPath := pathpkg.Join(p.url.Path, path)
263 target.Path = fullPath
264 target.RawPath = pathpkg.Join(target.RawPath, pathEscape(path))
266 resp, err := web.Get(web.DefaultSecurity, &target)
270 if err := resp.Err(); err != nil {
274 return resp.Body, nil
277 func (p *proxyRepo) Versions(prefix string) ([]string, error) {
278 data, err := p.getBytes("@v/list")
280 return nil, p.versionError("", err)
283 for _, line := range strings.Split(string(data), "\n") {
284 f := strings.Fields(line)
285 if len(f) >= 1 && semver.IsValid(f[0]) && strings.HasPrefix(f[0], prefix) && !IsPseudoVersion(f[0]) {
286 list = append(list, f[0])
293 func (p *proxyRepo) latest() (*RevInfo, error) {
294 data, err := p.getBytes("@v/list")
296 return nil, p.versionError("", err)
301 bestTimeIsFromPseudo bool
305 for _, line := range strings.Split(string(data), "\n") {
306 f := strings.Fields(line)
307 if len(f) >= 1 && semver.IsValid(f[0]) {
308 // If the proxy includes timestamps, prefer the timestamp it reports.
309 // Otherwise, derive the timestamp from the pseudo-version.
312 ftIsFromPseudo = false
315 ft, _ = time.Parse(time.RFC3339, f[1])
316 } else if IsPseudoVersion(f[0]) {
317 ft, _ = PseudoVersionTime(f[0])
318 ftIsFromPseudo = true
320 // Repo.Latest promises that this method is only called where there are
321 // no tagged versions. Ignore any tagged versions that were added in the
325 if bestTime.Before(ft) {
327 bestTimeIsFromPseudo = ftIsFromPseudo
332 if bestVersion == "" {
333 return nil, p.versionError("", codehost.ErrNoCommits)
336 if bestTimeIsFromPseudo {
337 // We parsed bestTime from the pseudo-version, but that's in UTC and we're
338 // supposed to report the timestamp as reported by the VCS.
339 // Stat the selected version to canonicalize the timestamp.
341 // TODO(bcmills): Should we also stat other versions to ensure that we
342 // report the correct Name and Short for the revision?
343 return p.Stat(bestVersion)
347 Version: bestVersion,
354 func (p *proxyRepo) Stat(rev string) (*RevInfo, error) {
355 encRev, err := module.EscapeVersion(rev)
357 return nil, p.versionError(rev, err)
359 data, err := p.getBytes("@v/" + encRev + ".info")
361 return nil, p.versionError(rev, err)
364 if err := json.Unmarshal(data, info); err != nil {
365 return nil, p.versionError(rev, err)
367 if info.Version != rev && rev == module.CanonicalVersion(rev) && module.Check(p.path, rev) == nil {
368 // If we request a correct, appropriate version for the module path, the
369 // proxy must return either exactly that version or an error — not some
370 // arbitrary other version.
371 return nil, p.versionError(rev, fmt.Errorf("proxy returned info for version %s instead of requested version", info.Version))
376 func (p *proxyRepo) Latest() (*RevInfo, error) {
377 data, err := p.getBytes("@latest")
379 if !errors.Is(err, os.ErrNotExist) {
380 return nil, p.versionError("", err)
385 if err := json.Unmarshal(data, info); err != nil {
386 return nil, p.versionError("", err)
391 func (p *proxyRepo) GoMod(version string) ([]byte, error) {
392 if version != module.CanonicalVersion(version) {
393 return nil, p.versionError(version, fmt.Errorf("internal error: version passed to GoMod is not canonical"))
396 encVer, err := module.EscapeVersion(version)
398 return nil, p.versionError(version, err)
400 data, err := p.getBytes("@v/" + encVer + ".mod")
402 return nil, p.versionError(version, err)
407 func (p *proxyRepo) Zip(dst io.Writer, version string) error {
408 if version != module.CanonicalVersion(version) {
409 return p.versionError(version, fmt.Errorf("internal error: version passed to Zip is not canonical"))
412 encVer, err := module.EscapeVersion(version)
414 return p.versionError(version, err)
416 body, err := p.getBody("@v/" + encVer + ".zip")
418 return p.versionError(version, err)
422 lr := &io.LimitedReader{R: body, N: codehost.MaxZipFile + 1}
423 if _, err := io.Copy(dst, lr); err != nil {
424 return p.versionError(version, err)
427 return p.versionError(version, fmt.Errorf("downloaded zip file too large"))
432 // pathEscape escapes s so it can be used in a path.
433 // That is, it escapes things like ? and # (which really shouldn't appear anyway).
434 // It does not escape / to %2F: our REST API is designed so that / can be left as is.
435 func pathEscape(s string) string {
436 return strings.ReplaceAll(url.PathEscape(s), "%2F", "/")