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