diff options
Diffstat (limited to 'src/cmd/go/internal/web/http.go')
-rw-r--r-- | src/cmd/go/internal/web/http.go | 360 |
1 files changed, 360 insertions, 0 deletions
diff --git a/src/cmd/go/internal/web/http.go b/src/cmd/go/internal/web/http.go new file mode 100644 index 0000000..a3b7787 --- /dev/null +++ b/src/cmd/go/internal/web/http.go @@ -0,0 +1,360 @@ +// 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. + +//go: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" + "net/http" + urlpkg "net/url" + "os" + "strings" + "time" + + "cmd/go/internal/auth" + "cmd/go/internal/cfg" + "cmd/internal/browser" +) + +// impatientInsecureHTTPClient is used with GOINSECURE, +// when we're connecting to https servers that might not be there +// or might be using self-signed certificates. +var impatientInsecureHTTPClient = &http.Client{ + CheckRedirect: checkRedirect, + Timeout: 5 * time.Second, + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + }, +} + +var securityPreservingDefaultClient = securityPreservingHTTPClient(http.DefaultClient) + +// securityPreservingDefaultClient returns a client that is like the original +// but rejects redirects to plain-HTTP URLs if the original URL was secure. +func securityPreservingHTTPClient(original *http.Client) *http.Client { + c := new(http.Client) + *c = *original + c.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) + } + return checkRedirect(req, via) + } + return c +} + +func checkRedirect(req *http.Request, via []*http.Request) error { + // Go's http.DefaultClient allows 10 redirects before returning an error. + // Mimic that behavior here. + if len(via) >= 10 { + return errors.New("stopped after 10 redirects") + } + + interceptRequest(req) + return nil +} + +type Interceptor struct { + Scheme string + FromHost string + ToHost string + Client *http.Client +} + +func EnableTestHooks(interceptors []Interceptor) error { + if enableTestHooks { + return errors.New("web: test hooks already enabled") + } + + for _, t := range interceptors { + if t.FromHost == "" { + panic("EnableTestHooks: missing FromHost") + } + if t.ToHost == "" { + panic("EnableTestHooks: missing ToHost") + } + } + + testInterceptors = interceptors + enableTestHooks = true + return nil +} + +func DisableTestHooks() { + if !enableTestHooks { + panic("web: test hooks not enabled") + } + enableTestHooks = false + testInterceptors = nil +} + +var ( + enableTestHooks = false + testInterceptors []Interceptor +) + +func interceptURL(u *urlpkg.URL) (*Interceptor, bool) { + if !enableTestHooks { + return nil, false + } + for i, t := range testInterceptors { + if u.Host == t.FromHost && (t.Scheme == "" || u.Scheme == t.Scheme) { + return &testInterceptors[i], true + } + } + return nil, false +} + +func interceptRequest(req *http.Request) { + if t, ok := interceptURL(req.URL); ok { + req.Host = req.URL.Host + req.URL.Host = t.ToHost + } +} + +func get(security SecurityMode, url *urlpkg.URL) (*Response, error) { + start := time.Now() + + if url.Scheme == "file" { + return getFile(url) + } + + if enableTestHooks { + switch url.Host { + case "proxy.golang.org": + if os.Getenv("TESTGOPROXY404") == "1" { + 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 + } + + case "localhost.localdev": + return nil, fmt.Errorf("no such host localhost.localdev") + + default: + if os.Getenv("TESTGONETWORK") == "panic" { + if _, ok := interceptURL(url); !ok { + host := url.Host + if h, _, err := net.SplitHostPort(url.Host); err == nil && h != "" { + host = h + } + addr := net.ParseIP(host) + if addr == nil || (!addr.IsLoopback() && !addr.IsUnspecified()) { + panic("use of network: " + url.String()) + } + } + } + } + } + + 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) + } + t, intercepted := interceptURL(req.URL) + if intercepted { + req.Host = req.URL.Host + req.URL.Host = t.ToHost + } + + var res *http.Response + if security == Insecure && url.Scheme == "https" { // fail earlier + res, err = impatientInsecureHTTPClient.Do(req) + } else { + if intercepted && t.Client != nil { + client := securityPreservingHTTPClient(t.Client) + res, err = client.Do(req) + } else { + res, err = securityPreservingDefaultClient.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) } + +func isLocalHost(u *urlpkg.URL) bool { + // VCSTestRepoURL itself is secure, and it may redirect requests to other + // ports (such as a port serving the "svn" protocol) which should also be + // considered secure. + host, _, err := net.SplitHostPort(u.Host) + if err != nil { + host = u.Host + } + if host == "localhost" { + return true + } + if ip := net.ParseIP(host); ip != nil && ip.IsLoopback() { + return true + } + return false +} |