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 }