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