Source file src/cmd/go/internal/web/http.go

     1  // Copyright 2012 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  //go:build !cmd_go_bootstrap
     6  
     7  // This code is compiled into the real 'go' binary, but it is not
     8  // compiled into the binary that is built during all.bash, so as
     9  // to avoid needing to build net (and thus use cgo) during the
    10  // bootstrap process.
    11  
    12  package web
    13  
    14  import (
    15  	"crypto/tls"
    16  	"errors"
    17  	"fmt"
    18  	"mime"
    19  	"net"
    20  	"net/http"
    21  	urlpkg "net/url"
    22  	"os"
    23  	"strings"
    24  	"time"
    25  
    26  	"cmd/go/internal/auth"
    27  	"cmd/go/internal/cfg"
    28  	"cmd/internal/browser"
    29  )
    30  
    31  // impatientInsecureHTTPClient is used with GOINSECURE,
    32  // when we're connecting to https servers that might not be there
    33  // or might be using self-signed certificates.
    34  var impatientInsecureHTTPClient = &http.Client{
    35  	Timeout: 5 * time.Second,
    36  	Transport: &http.Transport{
    37  		Proxy: http.ProxyFromEnvironment,
    38  		TLSClientConfig: &tls.Config{
    39  			InsecureSkipVerify: true,
    40  		},
    41  	},
    42  }
    43  
    44  // securityPreservingHTTPClient is like the default HTTP client, but rejects
    45  // redirects to plain-HTTP URLs if the original URL was secure.
    46  var securityPreservingHTTPClient = &http.Client{
    47  	CheckRedirect: func(req *http.Request, via []*http.Request) error {
    48  		if len(via) > 0 && via[0].URL.Scheme == "https" && req.URL.Scheme != "https" {
    49  			lastHop := via[len(via)-1].URL
    50  			return fmt.Errorf("redirected from secure URL %s to insecure URL %s", lastHop, req.URL)
    51  		}
    52  
    53  		// Go's http.DefaultClient allows 10 redirects before returning an error.
    54  		// The securityPreservingHTTPClient also uses this default policy to avoid
    55  		// Go command hangs.
    56  		if len(via) >= 10 {
    57  			return errors.New("stopped after 10 redirects")
    58  		}
    59  		return nil
    60  	},
    61  }
    62  
    63  func get(security SecurityMode, url *urlpkg.URL) (*Response, error) {
    64  	start := time.Now()
    65  
    66  	if url.Scheme == "file" {
    67  		return getFile(url)
    68  	}
    69  
    70  	if os.Getenv("TESTGOPROXY404") == "1" && url.Host == "proxy.golang.org" {
    71  		res := &Response{
    72  			URL:        url.Redacted(),
    73  			Status:     "404 testing",
    74  			StatusCode: 404,
    75  			Header:     make(map[string][]string),
    76  			Body:       http.NoBody,
    77  		}
    78  		if cfg.BuildX {
    79  			fmt.Fprintf(os.Stderr, "# get %s: %v (%.3fs)\n", url.Redacted(), res.Status, time.Since(start).Seconds())
    80  		}
    81  		return res, nil
    82  	}
    83  
    84  	if url.Host == "localhost.localdev" {
    85  		return nil, fmt.Errorf("no such host localhost.localdev")
    86  	}
    87  	if os.Getenv("TESTGONETWORK") == "panic" {
    88  		host := url.Host
    89  		if h, _, err := net.SplitHostPort(url.Host); err == nil && h != "" {
    90  			host = h
    91  		}
    92  		addr := net.ParseIP(host)
    93  		if addr == nil || (!addr.IsLoopback() && !addr.IsUnspecified()) {
    94  			panic("use of network: " + url.String())
    95  		}
    96  	}
    97  
    98  	fetch := func(url *urlpkg.URL) (*urlpkg.URL, *http.Response, error) {
    99  		// Note: The -v build flag does not mean "print logging information",
   100  		// despite its historical misuse for this in GOPATH-based go get.
   101  		// We print extra logging in -x mode instead, which traces what
   102  		// commands are executed.
   103  		if cfg.BuildX {
   104  			fmt.Fprintf(os.Stderr, "# get %s\n", url.Redacted())
   105  		}
   106  
   107  		req, err := http.NewRequest("GET", url.String(), nil)
   108  		if err != nil {
   109  			return nil, nil, err
   110  		}
   111  		if url.Scheme == "https" {
   112  			auth.AddCredentials(req)
   113  		}
   114  
   115  		var res *http.Response
   116  		if security == Insecure && url.Scheme == "https" { // fail earlier
   117  			res, err = impatientInsecureHTTPClient.Do(req)
   118  		} else {
   119  			res, err = securityPreservingHTTPClient.Do(req)
   120  		}
   121  		return url, res, err
   122  	}
   123  
   124  	var (
   125  		fetched *urlpkg.URL
   126  		res     *http.Response
   127  		err     error
   128  	)
   129  	if url.Scheme == "" || url.Scheme == "https" {
   130  		secure := new(urlpkg.URL)
   131  		*secure = *url
   132  		secure.Scheme = "https"
   133  
   134  		fetched, res, err = fetch(secure)
   135  		if err != nil {
   136  			if cfg.BuildX {
   137  				fmt.Fprintf(os.Stderr, "# get %s: %v\n", secure.Redacted(), err)
   138  			}
   139  			if security != Insecure || url.Scheme == "https" {
   140  				// HTTPS failed, and we can't fall back to plain HTTP.
   141  				// Report the error from the HTTPS attempt.
   142  				return nil, err
   143  			}
   144  		}
   145  	}
   146  
   147  	if res == nil {
   148  		switch url.Scheme {
   149  		case "http":
   150  			if security == SecureOnly {
   151  				if cfg.BuildX {
   152  					fmt.Fprintf(os.Stderr, "# get %s: insecure\n", url.Redacted())
   153  				}
   154  				return nil, fmt.Errorf("insecure URL: %s", url.Redacted())
   155  			}
   156  		case "":
   157  			if security != Insecure {
   158  				panic("should have returned after HTTPS failure")
   159  			}
   160  		default:
   161  			if cfg.BuildX {
   162  				fmt.Fprintf(os.Stderr, "# get %s: unsupported\n", url.Redacted())
   163  			}
   164  			return nil, fmt.Errorf("unsupported scheme: %s", url.Redacted())
   165  		}
   166  
   167  		insecure := new(urlpkg.URL)
   168  		*insecure = *url
   169  		insecure.Scheme = "http"
   170  		if insecure.User != nil && security != Insecure {
   171  			if cfg.BuildX {
   172  				fmt.Fprintf(os.Stderr, "# get %s: insecure credentials\n", insecure.Redacted())
   173  			}
   174  			return nil, fmt.Errorf("refusing to pass credentials to insecure URL: %s", insecure.Redacted())
   175  		}
   176  
   177  		fetched, res, err = fetch(insecure)
   178  		if err != nil {
   179  			if cfg.BuildX {
   180  				fmt.Fprintf(os.Stderr, "# get %s: %v\n", insecure.Redacted(), err)
   181  			}
   182  			// HTTP failed, and we already tried HTTPS if applicable.
   183  			// Report the error from the HTTP attempt.
   184  			return nil, err
   185  		}
   186  	}
   187  
   188  	// Note: accepting a non-200 OK here, so people can serve a
   189  	// meta import in their http 404 page.
   190  	if cfg.BuildX {
   191  		fmt.Fprintf(os.Stderr, "# get %s: %v (%.3fs)\n", fetched.Redacted(), res.Status, time.Since(start).Seconds())
   192  	}
   193  
   194  	r := &Response{
   195  		URL:        fetched.Redacted(),
   196  		Status:     res.Status,
   197  		StatusCode: res.StatusCode,
   198  		Header:     map[string][]string(res.Header),
   199  		Body:       res.Body,
   200  	}
   201  
   202  	if res.StatusCode != http.StatusOK {
   203  		contentType := res.Header.Get("Content-Type")
   204  		if mediaType, params, _ := mime.ParseMediaType(contentType); mediaType == "text/plain" {
   205  			switch charset := strings.ToLower(params["charset"]); charset {
   206  			case "us-ascii", "utf-8", "":
   207  				// Body claims to be plain text in UTF-8 or a subset thereof.
   208  				// Try to extract a useful error message from it.
   209  				r.errorDetail.r = res.Body
   210  				r.Body = &r.errorDetail
   211  			}
   212  		}
   213  	}
   214  
   215  	return r, nil
   216  }
   217  
   218  func getFile(u *urlpkg.URL) (*Response, error) {
   219  	path, err := urlToFilePath(u)
   220  	if err != nil {
   221  		return nil, err
   222  	}
   223  	f, err := os.Open(path)
   224  
   225  	if os.IsNotExist(err) {
   226  		return &Response{
   227  			URL:        u.Redacted(),
   228  			Status:     http.StatusText(http.StatusNotFound),
   229  			StatusCode: http.StatusNotFound,
   230  			Body:       http.NoBody,
   231  			fileErr:    err,
   232  		}, nil
   233  	}
   234  
   235  	if os.IsPermission(err) {
   236  		return &Response{
   237  			URL:        u.Redacted(),
   238  			Status:     http.StatusText(http.StatusForbidden),
   239  			StatusCode: http.StatusForbidden,
   240  			Body:       http.NoBody,
   241  			fileErr:    err,
   242  		}, nil
   243  	}
   244  
   245  	if err != nil {
   246  		return nil, err
   247  	}
   248  
   249  	return &Response{
   250  		URL:        u.Redacted(),
   251  		Status:     http.StatusText(http.StatusOK),
   252  		StatusCode: http.StatusOK,
   253  		Body:       f,
   254  	}, nil
   255  }
   256  
   257  func openBrowser(url string) bool { return browser.Open(url) }
   258  

View as plain text