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.
10package web
11
12import (
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".
26type SecurityMode int
27
28const (
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).
35type 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
43const (
44	maxErrorDetailLines = 8
45	maxErrorDetailBytes = maxErrorDetailLines * 81
46)
47
48func (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
72func (e *HTTPError) Is(target error) bool {
73	return target == fs.ErrNotExist && (e.StatusCode == 404 || e.StatusCode == 410)
74}
75
76func (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.
84func 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
100type 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.
114func (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.
130func (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// cmd/go/internal/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.)
185func 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.
190func 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.
196func 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.
208type errorDetailBuffer struct {
209	r        io.ReadCloser
210	buf      strings.Builder
211	bufLines int
212}
213
214func (b *errorDetailBuffer) Close() error {
215	return b.r.Close()
216}
217
218func (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".
244func IsLocalHost(u *url.URL) bool {
245	return isLocalHost(u)
246}
247