-
-
Notifications
You must be signed in to change notification settings - Fork 122
/
http.go
145 lines (123 loc) · 3 KB
/
http.go
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
package goproxy
import (
"context"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"path"
"path/filepath"
"strings"
)
var (
// errNotFound means something was not found.
errNotFound = errors.New("not found")
// errBadUpstream means an upstream is bad.
errBadUpstream = errors.New("bad upstream")
// errFetchTimedOut means a fetch operation has timed out.
errFetchTimedOut = errors.New("fetch timed out")
)
// notFoundError is an error indicating that something was not found.
type notFoundError struct {
error
}
// Is reports whether the err is `errNotFound`.
func (notFoundError) Is(err error) bool {
return err == errNotFound
}
// httpGet gets the content targeted by the url into the dst.
func httpGet(
ctx context.Context,
httpClient *http.Client,
url string,
dst io.Writer,
) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
}
res, err := httpClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode == http.StatusOK {
if dst != nil {
_, err := io.Copy(dst, res.Body)
return err
}
return nil
}
b, err := ioutil.ReadAll(res.Body)
if err != nil {
return err
}
switch res.StatusCode {
case http.StatusBadRequest, http.StatusNotFound, http.StatusGone:
return ¬FoundError{errors.New(string(b))}
case http.StatusBadGateway, http.StatusServiceUnavailable:
return errBadUpstream
case http.StatusGatewayTimeout:
return errFetchTimedOut
}
return fmt.Errorf("GET %s: %s: %s", redactedURL(req.URL), res.Status, b)
}
// parseRawURL parses the rawURL.
func parseRawURL(rawURL string) (*url.URL, error) {
if strings.ContainsAny(rawURL, ".:/") &&
!strings.Contains(rawURL, ":/") &&
!filepath.IsAbs(rawURL) &&
!path.IsAbs(rawURL) {
rawURL = fmt.Sprint("https://", rawURL)
}
u, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
switch u.Scheme {
case "http", "https":
default:
return nil, fmt.Errorf(
"invalid URL scheme (must be http or https): %s",
redactedURL(u),
)
}
return u, nil
}
// appendURL appends the extraPaths to the u safely and reutrns a new instance
// of the `url.URL`.
func appendURL(u *url.URL, extraPaths ...string) *url.URL {
nu := *u
u = &nu
for _, ep := range extraPaths {
if ep == "" {
continue
}
u.Path = path.Join(u.Path, ep)
u.RawPath = path.Join(
u.RawPath,
strings.ReplaceAll(url.PathEscape(ep), "%2F", "/"),
)
if ep[len(ep)-1] == '/' {
u.Path = fmt.Sprint(u.Path, "/")
u.RawPath = fmt.Sprint(u.RawPath, "/")
}
}
return u
}
// redactedURL returns a redacted string form of the u, suitable for printing in
// error messages. The string form replaces any non-empty password in the u with
// "xxxxx".
//
// TODO: Remove the `redactedURL` when the minimum supported Go version is 1.15.
// See https://golang.org/doc/go1.15#net/url.
func redactedURL(u *url.URL) string {
if _, ok := u.User.Password(); ok {
ru := *u
u = &ru
u.User = url.UserPassword(u.User.Username(), "xxxxx")
}
return u.String()
}