| // Copyright 2012 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // +build !cmd_go_bootstrap |
| |
| // This code is compiled into the real 'go' binary, but it is not |
| // compiled into the binary that is built during all.bash, so as |
| // to avoid needing to build net (and thus use cgo) during the |
| // bootstrap process. |
| |
| package web |
| |
| import ( |
| "crypto/tls" |
| "errors" |
| "fmt" |
| "mime" |
| "net/http" |
| urlpkg "net/url" |
| "os" |
| "strings" |
| "time" |
| |
| "cmd/go/internal/auth" |
| "cmd/go/internal/cfg" |
| "cmd/internal/browser" |
| ) |
| |
| // impatientInsecureHTTPClient is used in -insecure mode, |
| // when we're connecting to https servers that might not be there |
| // or might be using self-signed certificates. |
| var impatientInsecureHTTPClient = &http.Client{ |
| Timeout: 5 * time.Second, |
| Transport: &http.Transport{ |
| Proxy: http.ProxyFromEnvironment, |
| TLSClientConfig: &tls.Config{ |
| InsecureSkipVerify: true, |
| }, |
| }, |
| } |
| |
| // securityPreservingHTTPClient is like the default HTTP client, but rejects |
| // redirects to plain-HTTP URLs if the original URL was secure. |
| var securityPreservingHTTPClient = &http.Client{ |
| CheckRedirect: func(req *http.Request, via []*http.Request) error { |
| if len(via) > 0 && via[0].URL.Scheme == "https" && req.URL.Scheme != "https" { |
| lastHop := via[len(via)-1].URL |
| return fmt.Errorf("redirected from secure URL %s to insecure URL %s", lastHop, req.URL) |
| } |
| |
| // Go's http.DefaultClient allows 10 redirects before returning an error. |
| // The securityPreservingHTTPClient also uses this default policy to avoid |
| // Go command hangs. |
| if len(via) >= 10 { |
| return errors.New("stopped after 10 redirects") |
| } |
| return nil |
| }, |
| } |
| |
| func get(security SecurityMode, url *urlpkg.URL) (*Response, error) { |
| start := time.Now() |
| |
| if url.Scheme == "file" { |
| return getFile(url) |
| } |
| |
| if os.Getenv("TESTGOPROXY404") == "1" && url.Host == "proxy.golang.org" { |
| res := &Response{ |
| URL: url.Redacted(), |
| Status: "404 testing", |
| StatusCode: 404, |
| Header: make(map[string][]string), |
| Body: http.NoBody, |
| } |
| if cfg.BuildX { |
| fmt.Fprintf(os.Stderr, "# get %s: %v (%.3fs)\n", url.Redacted(), res.Status, time.Since(start).Seconds()) |
| } |
| return res, nil |
| } |
| |
| fetch := func(url *urlpkg.URL) (*urlpkg.URL, *http.Response, error) { |
| // Note: The -v build flag does not mean "print logging information", |
| // despite its historical misuse for this in GOPATH-based go get. |
| // We print extra logging in -x mode instead, which traces what |
| // commands are executed. |
| if cfg.BuildX { |
| fmt.Fprintf(os.Stderr, "# get %s\n", url.Redacted()) |
| } |
| |
| req, err := http.NewRequest("GET", url.String(), nil) |
| if err != nil { |
| return nil, nil, err |
| } |
| if url.Scheme == "https" { |
| auth.AddCredentials(req) |
| } |
| |
| var res *http.Response |
| if security == Insecure && url.Scheme == "https" { // fail earlier |
| res, err = impatientInsecureHTTPClient.Do(req) |
| } else { |
| res, err = securityPreservingHTTPClient.Do(req) |
| } |
| return url, res, err |
| } |
| |
| var ( |
| fetched *urlpkg.URL |
| res *http.Response |
| err error |
| ) |
| if url.Scheme == "" || url.Scheme == "https" { |
| secure := new(urlpkg.URL) |
| *secure = *url |
| secure.Scheme = "https" |
| |
| fetched, res, err = fetch(secure) |
| if err != nil { |
| if cfg.BuildX { |
| fmt.Fprintf(os.Stderr, "# get %s: %v\n", secure.Redacted(), err) |
| } |
| if security != Insecure || url.Scheme == "https" { |
| // HTTPS failed, and we can't fall back to plain HTTP. |
| // Report the error from the HTTPS attempt. |
| return nil, err |
| } |
| } |
| } |
| |
| if res == nil { |
| switch url.Scheme { |
| case "http": |
| if security == SecureOnly { |
| if cfg.BuildX { |
| fmt.Fprintf(os.Stderr, "# get %s: insecure\n", url.Redacted()) |
| } |
| return nil, fmt.Errorf("insecure URL: %s", url.Redacted()) |
| } |
| case "": |
| if security != Insecure { |
| panic("should have returned after HTTPS failure") |
| } |
| default: |
| if cfg.BuildX { |
| fmt.Fprintf(os.Stderr, "# get %s: unsupported\n", url.Redacted()) |
| } |
| return nil, fmt.Errorf("unsupported scheme: %s", url.Redacted()) |
| } |
| |
| insecure := new(urlpkg.URL) |
| *insecure = *url |
| insecure.Scheme = "http" |
| if insecure.User != nil && security != Insecure { |
| if cfg.BuildX { |
| fmt.Fprintf(os.Stderr, "# get %s: insecure credentials\n", insecure.Redacted()) |
| } |
| return nil, fmt.Errorf("refusing to pass credentials to insecure URL: %s", insecure.Redacted()) |
| } |
| |
| fetched, res, err = fetch(insecure) |
| if err != nil { |
| if cfg.BuildX { |
| fmt.Fprintf(os.Stderr, "# get %s: %v\n", insecure.Redacted(), err) |
| } |
| // HTTP failed, and we already tried HTTPS if applicable. |
| // Report the error from the HTTP attempt. |
| return nil, err |
| } |
| } |
| |
| // Note: accepting a non-200 OK here, so people can serve a |
| // meta import in their http 404 page. |
| if cfg.BuildX { |
| fmt.Fprintf(os.Stderr, "# get %s: %v (%.3fs)\n", fetched.Redacted(), res.Status, time.Since(start).Seconds()) |
| } |
| |
| r := &Response{ |
| URL: fetched.Redacted(), |
| Status: res.Status, |
| StatusCode: res.StatusCode, |
| Header: map[string][]string(res.Header), |
| Body: res.Body, |
| } |
| |
| if res.StatusCode != http.StatusOK { |
| contentType := res.Header.Get("Content-Type") |
| if mediaType, params, _ := mime.ParseMediaType(contentType); mediaType == "text/plain" { |
| switch charset := strings.ToLower(params["charset"]); charset { |
| case "us-ascii", "utf-8", "": |
| // Body claims to be plain text in UTF-8 or a subset thereof. |
| // Try to extract a useful error message from it. |
| r.errorDetail.r = res.Body |
| r.Body = &r.errorDetail |
| } |
| } |
| } |
| |
| return r, nil |
| } |
| |
| func getFile(u *urlpkg.URL) (*Response, error) { |
| path, err := urlToFilePath(u) |
| if err != nil { |
| return nil, err |
| } |
| f, err := os.Open(path) |
| |
| if os.IsNotExist(err) { |
| return &Response{ |
| URL: u.Redacted(), |
| Status: http.StatusText(http.StatusNotFound), |
| StatusCode: http.StatusNotFound, |
| Body: http.NoBody, |
| fileErr: err, |
| }, nil |
| } |
| |
| if os.IsPermission(err) { |
| return &Response{ |
| URL: u.Redacted(), |
| Status: http.StatusText(http.StatusForbidden), |
| StatusCode: http.StatusForbidden, |
| Body: http.NoBody, |
| fileErr: err, |
| }, nil |
| } |
| |
| if err != nil { |
| return nil, err |
| } |
| |
| return &Response{ |
| URL: u.Redacted(), |
| Status: http.StatusText(http.StatusOK), |
| StatusCode: http.StatusOK, |
| Body: f, |
| }, nil |
| } |
| |
| func openBrowser(url string) bool { return browser.Open(url) } |