github.com/gagliardetto/golang-go@v0.0.0-20201020153340-53909ea70814/cmd/go/not-internal/web/api.go (about)

     1  // Copyright 2017 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 web defines minimal helper routines for accessing HTTP/HTTPS
     6  // resources without requiring external dependencies on the net package.
     7  //
     8  // If the cmd_go_bootstrap build tag is present, web avoids the use of the net
     9  // package and returns errors for all network operations.
    10  package web
    11  
    12  import (
    13  	"bytes"
    14  	"fmt"
    15  	"io"
    16  	"io/ioutil"
    17  	"net/url"
    18  	"os"
    19  	"strings"
    20  	"unicode"
    21  	"unicode/utf8"
    22  )
    23  
    24  // SecurityMode specifies whether a function should make network
    25  // calls using insecure transports (eg, plain text HTTP).
    26  // The zero value is "secure".
    27  type SecurityMode int
    28  
    29  const (
    30  	SecureOnly      SecurityMode = iota // Reject plain HTTP; validate HTTPS.
    31  	DefaultSecurity                     // Allow plain HTTP if explicit; validate HTTPS.
    32  	Insecure                            // Allow plain HTTP if not explicitly HTTPS; skip HTTPS validation.
    33  )
    34  
    35  // An HTTPError describes an HTTP error response (non-200 result).
    36  type HTTPError struct {
    37  	URL        string // redacted
    38  	Status     string
    39  	StatusCode int
    40  	Err        error  // underlying error, if known
    41  	Detail     string // limited to maxErrorDetailLines and maxErrorDetailBytes
    42  }
    43  
    44  const (
    45  	maxErrorDetailLines = 8
    46  	maxErrorDetailBytes = maxErrorDetailLines * 81
    47  )
    48  
    49  func (e *HTTPError) Error() string {
    50  	if e.Detail != "" {
    51  		detailSep := " "
    52  		if strings.ContainsRune(e.Detail, '\n') {
    53  			detailSep = "\n\t"
    54  		}
    55  		return fmt.Sprintf("reading %s: %v\n\tserver response:%s%s", e.URL, e.Status, detailSep, e.Detail)
    56  	}
    57  
    58  	if err := e.Err; err != nil {
    59  		if pErr, ok := e.Err.(*os.PathError); ok && strings.HasSuffix(e.URL, pErr.Path) {
    60  			// Remove the redundant copy of the path.
    61  			err = pErr.Err
    62  		}
    63  		return fmt.Sprintf("reading %s: %v", e.URL, err)
    64  	}
    65  
    66  	return fmt.Sprintf("reading %s: %v", e.URL, e.Status)
    67  }
    68  
    69  func (e *HTTPError) Is(target error) bool {
    70  	return target == os.ErrNotExist && (e.StatusCode == 404 || e.StatusCode == 410)
    71  }
    72  
    73  func (e *HTTPError) Unwrap() error {
    74  	return e.Err
    75  }
    76  
    77  // GetBytes returns the body of the requested resource, or an error if the
    78  // response status was not http.StatusOK.
    79  //
    80  // GetBytes is a convenience wrapper around Get and Response.Err.
    81  func GetBytes(u *url.URL) ([]byte, error) {
    82  	resp, err := Get(DefaultSecurity, u)
    83  	if err != nil {
    84  		return nil, err
    85  	}
    86  	defer resp.Body.Close()
    87  	if err := resp.Err(); err != nil {
    88  		return nil, err
    89  	}
    90  	b, err := ioutil.ReadAll(resp.Body)
    91  	if err != nil {
    92  		return nil, fmt.Errorf("reading %s: %v", Redacted(u), err)
    93  	}
    94  	return b, nil
    95  }
    96  
    97  type Response struct {
    98  	URL        string // redacted
    99  	Status     string
   100  	StatusCode int
   101  	Header     map[string][]string
   102  	Body       io.ReadCloser // Either the original body or &errorDetail.
   103  
   104  	fileErr     error
   105  	errorDetail errorDetailBuffer
   106  }
   107  
   108  // Err returns an *HTTPError corresponding to the response r.
   109  // If the response r has StatusCode 200 or 0 (unset), Err returns nil.
   110  // Otherwise, Err may read from r.Body in order to extract relevant error detail.
   111  func (r *Response) Err() error {
   112  	if r.StatusCode == 200 || r.StatusCode == 0 {
   113  		return nil
   114  	}
   115  
   116  	return &HTTPError{
   117  		URL:        r.URL,
   118  		Status:     r.Status,
   119  		StatusCode: r.StatusCode,
   120  		Err:        r.fileErr,
   121  		Detail:     r.formatErrorDetail(),
   122  	}
   123  }
   124  
   125  // formatErrorDetail converts r.errorDetail (a prefix of the output of r.Body)
   126  // into a short, tab-indented summary.
   127  func (r *Response) formatErrorDetail() string {
   128  	if r.Body != &r.errorDetail {
   129  		return "" // Error detail collection not enabled.
   130  	}
   131  
   132  	// Ensure that r.errorDetail has been populated.
   133  	_, _ = io.Copy(ioutil.Discard, r.Body)
   134  
   135  	s := r.errorDetail.buf.String()
   136  	if !utf8.ValidString(s) {
   137  		return "" // Don't try to recover non-UTF-8 error messages.
   138  	}
   139  	for _, r := range s {
   140  		if !unicode.IsGraphic(r) && !unicode.IsSpace(r) {
   141  			return "" // Don't let the server do any funny business with the user's terminal.
   142  		}
   143  	}
   144  
   145  	var detail strings.Builder
   146  	for i, line := range strings.Split(s, "\n") {
   147  		if strings.TrimSpace(line) == "" {
   148  			break // Stop at the first blank line.
   149  		}
   150  		if i > 0 {
   151  			detail.WriteString("\n\t")
   152  		}
   153  		if i >= maxErrorDetailLines {
   154  			detail.WriteString("[Truncated: too many lines.]")
   155  			break
   156  		}
   157  		if detail.Len()+len(line) > maxErrorDetailBytes {
   158  			detail.WriteString("[Truncated: too long.]")
   159  			break
   160  		}
   161  		detail.WriteString(line)
   162  	}
   163  
   164  	return detail.String()
   165  }
   166  
   167  // Get returns the body of the HTTP or HTTPS resource specified at the given URL.
   168  //
   169  // If the URL does not include an explicit scheme, Get first tries "https".
   170  // If the server does not respond under that scheme and the security mode is
   171  // Insecure, Get then tries "http".
   172  // The URL included in the response indicates which scheme was actually used,
   173  // and it is a redacted URL suitable for use in error messages.
   174  //
   175  // For the "https" scheme only, credentials are attached using the
   176  // cmd/go/internal/auth package. If the URL itself includes a username and
   177  // password, it will not be attempted under the "http" scheme unless the
   178  // security mode is Insecure.
   179  //
   180  // Get returns a non-nil error only if the request did not receive a response
   181  // under any applicable scheme. (A non-2xx response does not cause an error.)
   182  func Get(security SecurityMode, u *url.URL) (*Response, error) {
   183  	return get(security, u)
   184  }
   185  
   186  // Redacted returns a redacted string form of the URL,
   187  // suitable for printing in error messages.
   188  // The string form replaces any non-empty password
   189  // in the original URL with "[redacted]".
   190  func Redacted(u *url.URL) string {
   191  	if u.User != nil {
   192  		if _, ok := u.User.Password(); ok {
   193  			redacted := *u
   194  			redacted.User = url.UserPassword(u.User.Username(), "[redacted]")
   195  			u = &redacted
   196  		}
   197  	}
   198  	return u.String()
   199  }
   200  
   201  // OpenBrowser attempts to open the requested URL in a web browser.
   202  func OpenBrowser(url string) (opened bool) {
   203  	return openBrowser(url)
   204  }
   205  
   206  // Join returns the result of adding the slash-separated
   207  // path elements to the end of u's path.
   208  func Join(u *url.URL, path string) *url.URL {
   209  	j := *u
   210  	if path == "" {
   211  		return &j
   212  	}
   213  	j.Path = strings.TrimSuffix(u.Path, "/") + "/" + strings.TrimPrefix(path, "/")
   214  	j.RawPath = strings.TrimSuffix(u.RawPath, "/") + "/" + strings.TrimPrefix(path, "/")
   215  	return &j
   216  }
   217  
   218  // An errorDetailBuffer is an io.ReadCloser that copies up to
   219  // maxErrorDetailLines into a buffer for later inspection.
   220  type errorDetailBuffer struct {
   221  	r        io.ReadCloser
   222  	buf      strings.Builder
   223  	bufLines int
   224  }
   225  
   226  func (b *errorDetailBuffer) Close() error {
   227  	return b.r.Close()
   228  }
   229  
   230  func (b *errorDetailBuffer) Read(p []byte) (n int, err error) {
   231  	n, err = b.r.Read(p)
   232  
   233  	// Copy the first maxErrorDetailLines+1 lines into b.buf,
   234  	// discarding any further lines.
   235  	//
   236  	// Note that the read may begin or end in the middle of a UTF-8 character,
   237  	// so don't try to do anything fancy with characters that encode to larger
   238  	// than one byte.
   239  	if b.bufLines <= maxErrorDetailLines {
   240  		for _, line := range bytes.SplitAfterN(p[:n], []byte("\n"), maxErrorDetailLines-b.bufLines) {
   241  			b.buf.Write(line)
   242  			if len(line) > 0 && line[len(line)-1] == '\n' {
   243  				b.bufLines++
   244  				if b.bufLines > maxErrorDetailLines {
   245  					break
   246  				}
   247  			}
   248  		}
   249  	}
   250  
   251  	return n, err
   252  }