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