| // 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" |
| "io" |
| "mime" |
| "net" |
| "net/http" |
| urlpkg "net/url" |
| "os" |
| "strings" |
| "time" |
| |
| "cmd/go/internal/auth" |
| "cmd/go/internal/base" |
| "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) |
| |
| // securityPreservingHTTPClient 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 && (u.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) (*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, 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 |
| } |
| |
| release, err := base.AcquireNet() |
| if err != nil { |
| return nil, err |
| } |
| |
| 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) |
| } |
| } |
| |
| if err != nil { |
| // Per the docs for [net/http.Client.Do], “On error, any Response can be |
| // ignored. A non-nil Response with a non-nil error only occurs when |
| // CheckRedirect fails, and even then the returned Response.Body is |
| // already closed.” |
| release() |
| return nil, err |
| } |
| |
| // “If the returned error is nil, the Response will contain a non-nil Body |
| // which the user is expected to close.” |
| body := res.Body |
| res.Body = hookCloser{ |
| ReadCloser: body, |
| afterClose: release, |
| } |
| return 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" |
| |
| res, err = fetch(secure) |
| if err == nil { |
| fetched = secure |
| } else { |
| 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()) |
| } |
| |
| res, err = fetch(insecure) |
| if err == nil { |
| fetched = insecure |
| } else { |
| 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 |
| } |
| |
| type hookCloser struct { |
| io.ReadCloser |
| afterClose func() |
| } |
| |
| func (c hookCloser) Close() error { |
| err := c.ReadCloser.Close() |
| c.afterClose() |
| return err |
| } |