github.com/bir3/gocompiler@v0.3.205/src/cmd/gocmd/internal/modfetch/proxy.go (about) 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 "encoding/json" 9 "errors" 10 "fmt" 11 "io" 12 "io/fs" 13 "net/url" 14 "path" 15 pathpkg "path" 16 "path/filepath" 17 "strings" 18 "sync" 19 "time" 20 21 "github.com/bir3/gocompiler/src/cmd/gocmd/internal/base" 22 "github.com/bir3/gocompiler/src/cmd/gocmd/internal/cfg" 23 "github.com/bir3/gocompiler/src/cmd/gocmd/internal/modfetch/codehost" 24 "github.com/bir3/gocompiler/src/cmd/gocmd/internal/web" 25 26 "github.com/bir3/gocompiler/src/xvendor/golang.org/x/mod/module" 27 "github.com/bir3/gocompiler/src/xvendor/golang.org/x/mod/semver" 28 ) 29 30 var HelpGoproxy = &base.Command{ 31 UsageLine: "goproxy", 32 Short: "module proxy protocol", 33 Long: ` 34 A Go module proxy is any web server that can respond to GET requests for 35 URLs of a specified form. The requests have no query parameters, so even 36 a site serving from a fixed file system (including a file:/// URL) 37 can be a module proxy. 38 39 For details on the GOPROXY protocol, see 40 https://golang.org/ref/mod#goproxy-protocol. 41 `, 42 } 43 44 var proxyOnce struct { 45 sync.Once 46 list []proxySpec 47 err error 48 } 49 50 type proxySpec struct { 51 // url is the proxy URL or one of "off", "direct", "noproxy". 52 url string 53 54 // fallBackOnError is true if a request should be attempted on the next proxy 55 // in the list after any error from this proxy. If fallBackOnError is false, 56 // the request will only be attempted on the next proxy if the error is 57 // equivalent to os.ErrNotFound, which is true for 404 and 410 responses. 58 fallBackOnError bool 59 } 60 61 func proxyList() ([]proxySpec, error) { 62 proxyOnce.Do(func() { 63 if cfg.GONOPROXY != "" && cfg.GOPROXY != "direct" { 64 proxyOnce.list = append(proxyOnce.list, proxySpec{url: "noproxy"}) 65 } 66 67 goproxy := cfg.GOPROXY 68 for goproxy != "" { 69 var url string 70 fallBackOnError := false 71 if i := strings.IndexAny(goproxy, ",|"); i >= 0 { 72 url = goproxy[:i] 73 fallBackOnError = goproxy[i] == '|' 74 goproxy = goproxy[i+1:] 75 } else { 76 url = goproxy 77 goproxy = "" 78 } 79 80 url = strings.TrimSpace(url) 81 if url == "" { 82 continue 83 } 84 if url == "off" { 85 // "off" always fails hard, so can stop walking list. 86 proxyOnce.list = append(proxyOnce.list, proxySpec{url: "off"}) 87 break 88 } 89 if url == "direct" { 90 proxyOnce.list = append(proxyOnce.list, proxySpec{url: "direct"}) 91 // For now, "direct" is the end of the line. We may decide to add some 92 // sort of fallback behavior for them in the future, so ignore 93 // subsequent entries for forward-compatibility. 94 break 95 } 96 97 // Single-word tokens are reserved for built-in behaviors, and anything 98 // containing the string ":/" or matching an absolute file path must be a 99 // complete URL. For all other paths, implicitly add "https://". 100 if strings.ContainsAny(url, ".:/") && !strings.Contains(url, ":/") && !filepath.IsAbs(url) && !path.IsAbs(url) { 101 url = "https://" + url 102 } 103 104 // Check that newProxyRepo accepts the URL. 105 // It won't do anything with the path. 106 if _, err := newProxyRepo(url, "golang.org/x/text"); err != nil { 107 proxyOnce.err = err 108 return 109 } 110 111 proxyOnce.list = append(proxyOnce.list, proxySpec{ 112 url: url, 113 fallBackOnError: fallBackOnError, 114 }) 115 } 116 117 if len(proxyOnce.list) == 0 || 118 len(proxyOnce.list) == 1 && proxyOnce.list[0].url == "noproxy" { 119 // There were no proxies, other than the implicit "noproxy" added when 120 // GONOPROXY is set. This can happen if GOPROXY is a non-empty string 121 // like "," or " ". 122 proxyOnce.err = fmt.Errorf("GOPROXY list is not the empty string, but contains no entries") 123 } 124 }) 125 126 return proxyOnce.list, proxyOnce.err 127 } 128 129 // TryProxies iterates f over each configured proxy (including "noproxy" and 130 // "direct" if applicable) until f returns no error or until f returns an 131 // error that is not equivalent to fs.ErrNotExist on a proxy configured 132 // not to fall back on errors. 133 // 134 // TryProxies then returns that final error. 135 // 136 // If GOPROXY is set to "off", TryProxies invokes f once with the argument 137 // "off". 138 func TryProxies(f func(proxy string) error) error { 139 proxies, err := proxyList() 140 if err != nil { 141 return err 142 } 143 if len(proxies) == 0 { 144 panic("GOPROXY list is empty") 145 } 146 147 // We try to report the most helpful error to the user. "direct" and "noproxy" 148 // errors are best, followed by proxy errors other than ErrNotExist, followed 149 // by ErrNotExist. 150 // 151 // Note that errProxyOff, errNoproxy, and errUseProxy are equivalent to 152 // ErrNotExist. errUseProxy should only be returned if "noproxy" is the only 153 // proxy. errNoproxy should never be returned, since there should always be a 154 // more useful error from "noproxy" first. 155 const ( 156 notExistRank = iota 157 proxyRank 158 directRank 159 ) 160 var bestErr error 161 bestErrRank := notExistRank 162 for _, proxy := range proxies { 163 err := f(proxy.url) 164 if err == nil { 165 return nil 166 } 167 isNotExistErr := errors.Is(err, fs.ErrNotExist) 168 169 if proxy.url == "direct" || (proxy.url == "noproxy" && err != errUseProxy) { 170 bestErr = err 171 bestErrRank = directRank 172 } else if bestErrRank <= proxyRank && !isNotExistErr { 173 bestErr = err 174 bestErrRank = proxyRank 175 } else if bestErrRank == notExistRank { 176 bestErr = err 177 } 178 179 if !proxy.fallBackOnError && !isNotExistErr { 180 break 181 } 182 } 183 return bestErr 184 } 185 186 type proxyRepo struct { 187 url *url.URL 188 path string 189 redactedURL string 190 191 listLatestOnce sync.Once 192 listLatest *RevInfo 193 listLatestErr error 194 } 195 196 func newProxyRepo(baseURL, path string) (Repo, error) { 197 base, err := url.Parse(baseURL) 198 if err != nil { 199 return nil, err 200 } 201 switch base.Scheme { 202 case "http", "https": 203 // ok 204 case "file": 205 if *base != (url.URL{Scheme: base.Scheme, Path: base.Path, RawPath: base.RawPath}) { 206 return nil, fmt.Errorf("invalid file:// proxy URL with non-path elements: %s", base.Redacted()) 207 } 208 case "": 209 return nil, fmt.Errorf("invalid proxy URL missing scheme: %s", base.Redacted()) 210 default: 211 return nil, fmt.Errorf("invalid proxy URL scheme (must be https, http, file): %s", base.Redacted()) 212 } 213 214 enc, err := module.EscapePath(path) 215 if err != nil { 216 return nil, err 217 } 218 redactedURL := base.Redacted() 219 base.Path = strings.TrimSuffix(base.Path, "/") + "/" + enc 220 base.RawPath = strings.TrimSuffix(base.RawPath, "/") + "/" + pathEscape(enc) 221 return &proxyRepo{base, path, redactedURL, sync.Once{}, nil, nil}, nil 222 } 223 224 func (p *proxyRepo) ModulePath() string { 225 return p.path 226 } 227 228 var errProxyReuse = fmt.Errorf("proxy does not support CheckReuse") 229 230 func (p *proxyRepo) CheckReuse(old *codehost.Origin) error { 231 return errProxyReuse 232 } 233 234 // versionError returns err wrapped in a ModuleError for p.path. 235 func (p *proxyRepo) versionError(version string, err error) error { 236 if version != "" && version != module.CanonicalVersion(version) { 237 return &module.ModuleError{ 238 Path: p.path, 239 Err: &module.InvalidVersionError{ 240 Version: version, 241 Pseudo: module.IsPseudoVersion(version), 242 Err: err, 243 }, 244 } 245 } 246 247 return &module.ModuleError{ 248 Path: p.path, 249 Version: version, 250 Err: err, 251 } 252 } 253 254 func (p *proxyRepo) getBytes(path string) ([]byte, error) { 255 body, err := p.getBody(path) 256 if err != nil { 257 return nil, err 258 } 259 defer body.Close() 260 261 b, err := io.ReadAll(body) 262 if err != nil { 263 // net/http doesn't add context to Body errors, so add it here. 264 // (See https://go.dev/issue/52727.) 265 return b, &url.Error{Op: "read", URL: pathpkg.Join(p.redactedURL, path), Err: err} 266 } 267 return b, nil 268 } 269 270 func (p *proxyRepo) getBody(path string) (r io.ReadCloser, err error) { 271 fullPath := pathpkg.Join(p.url.Path, path) 272 273 target := *p.url 274 target.Path = fullPath 275 target.RawPath = pathpkg.Join(target.RawPath, pathEscape(path)) 276 277 resp, err := web.Get(web.DefaultSecurity, &target) 278 if err != nil { 279 return nil, err 280 } 281 if err := resp.Err(); err != nil { 282 resp.Body.Close() 283 return nil, err 284 } 285 return resp.Body, nil 286 } 287 288 func (p *proxyRepo) Versions(prefix string) (*Versions, error) { 289 data, err := p.getBytes("@v/list") 290 if err != nil { 291 p.listLatestOnce.Do(func() { 292 p.listLatest, p.listLatestErr = nil, p.versionError("", err) 293 }) 294 return nil, p.versionError("", err) 295 } 296 var list []string 297 allLine := strings.Split(string(data), "\n") 298 for _, line := range allLine { 299 f := strings.Fields(line) 300 if len(f) >= 1 && semver.IsValid(f[0]) && strings.HasPrefix(f[0], prefix) && !module.IsPseudoVersion(f[0]) { 301 list = append(list, f[0]) 302 } 303 } 304 p.listLatestOnce.Do(func() { 305 p.listLatest, p.listLatestErr = p.latestFromList(allLine) 306 }) 307 semver.Sort(list) 308 return &Versions{List: list}, nil 309 } 310 311 func (p *proxyRepo) latest() (*RevInfo, error) { 312 p.listLatestOnce.Do(func() { 313 data, err := p.getBytes("@v/list") 314 if err != nil { 315 p.listLatestErr = p.versionError("", err) 316 return 317 } 318 list := strings.Split(string(data), "\n") 319 p.listLatest, p.listLatestErr = p.latestFromList(list) 320 }) 321 return p.listLatest, p.listLatestErr 322 } 323 324 func (p *proxyRepo) latestFromList(allLine []string) (*RevInfo, error) { 325 var ( 326 bestTime time.Time 327 bestVersion string 328 ) 329 for _, line := range allLine { 330 f := strings.Fields(line) 331 if len(f) >= 1 && semver.IsValid(f[0]) { 332 // If the proxy includes timestamps, prefer the timestamp it reports. 333 // Otherwise, derive the timestamp from the pseudo-version. 334 var ( 335 ft time.Time 336 ) 337 if len(f) >= 2 { 338 ft, _ = time.Parse(time.RFC3339, f[1]) 339 } else if module.IsPseudoVersion(f[0]) { 340 ft, _ = module.PseudoVersionTime(f[0]) 341 } else { 342 // Repo.Latest promises that this method is only called where there are 343 // no tagged versions. Ignore any tagged versions that were added in the 344 // meantime. 345 continue 346 } 347 if bestTime.Before(ft) { 348 bestTime = ft 349 bestVersion = f[0] 350 } 351 } 352 } 353 if bestVersion == "" { 354 return nil, p.versionError("", codehost.ErrNoCommits) 355 } 356 357 // Call Stat to get all the other fields, including Origin information. 358 return p.Stat(bestVersion) 359 } 360 361 func (p *proxyRepo) Stat(rev string) (*RevInfo, error) { 362 encRev, err := module.EscapeVersion(rev) 363 if err != nil { 364 return nil, p.versionError(rev, err) 365 } 366 data, err := p.getBytes("@v/" + encRev + ".info") 367 if err != nil { 368 return nil, p.versionError(rev, err) 369 } 370 info := new(RevInfo) 371 if err := json.Unmarshal(data, info); err != nil { 372 return nil, p.versionError(rev, fmt.Errorf("invalid response from proxy %q: %w", p.redactedURL, err)) 373 } 374 if info.Version != rev && rev == module.CanonicalVersion(rev) && module.Check(p.path, rev) == nil { 375 // If we request a correct, appropriate version for the module path, the 376 // proxy must return either exactly that version or an error — not some 377 // arbitrary other version. 378 return nil, p.versionError(rev, fmt.Errorf("proxy returned info for version %s instead of requested version", info.Version)) 379 } 380 return info, nil 381 } 382 383 func (p *proxyRepo) Latest() (*RevInfo, error) { 384 data, err := p.getBytes("@latest") 385 if err != nil { 386 if !errors.Is(err, fs.ErrNotExist) { 387 return nil, p.versionError("", err) 388 } 389 return p.latest() 390 } 391 info := new(RevInfo) 392 if err := json.Unmarshal(data, info); err != nil { 393 return nil, p.versionError("", fmt.Errorf("invalid response from proxy %q: %w", p.redactedURL, err)) 394 } 395 return info, nil 396 } 397 398 func (p *proxyRepo) GoMod(version string) ([]byte, error) { 399 if version != module.CanonicalVersion(version) { 400 return nil, p.versionError(version, fmt.Errorf("internal error: version passed to GoMod is not canonical")) 401 } 402 403 encVer, err := module.EscapeVersion(version) 404 if err != nil { 405 return nil, p.versionError(version, err) 406 } 407 data, err := p.getBytes("@v/" + encVer + ".mod") 408 if err != nil { 409 return nil, p.versionError(version, err) 410 } 411 return data, nil 412 } 413 414 func (p *proxyRepo) Zip(dst io.Writer, version string) error { 415 if version != module.CanonicalVersion(version) { 416 return p.versionError(version, fmt.Errorf("internal error: version passed to Zip is not canonical")) 417 } 418 419 encVer, err := module.EscapeVersion(version) 420 if err != nil { 421 return p.versionError(version, err) 422 } 423 path := "@v/" + encVer + ".zip" 424 body, err := p.getBody(path) 425 if err != nil { 426 return p.versionError(version, err) 427 } 428 defer body.Close() 429 430 lr := &io.LimitedReader{R: body, N: codehost.MaxZipFile + 1} 431 if _, err := io.Copy(dst, lr); err != nil { 432 // net/http doesn't add context to Body errors, so add it here. 433 // (See https://go.dev/issue/52727.) 434 err = &url.Error{Op: "read", URL: pathpkg.Join(p.redactedURL, path), Err: err} 435 return p.versionError(version, err) 436 } 437 if lr.N <= 0 { 438 return p.versionError(version, fmt.Errorf("downloaded zip file too large")) 439 } 440 return nil 441 } 442 443 // pathEscape escapes s so it can be used in a path. 444 // That is, it escapes things like ? and # (which really shouldn't appear anyway). 445 // It does not escape / to %2F: our REST API is designed so that / can be left as is. 446 func pathEscape(s string) string { 447 return strings.ReplaceAll(url.PathEscape(s), "%2F", "/") 448 }