1
2
3
4
5
6
7
8
9
10
11
12 package web
13
14 import (
15 "crypto/tls"
16 "errors"
17 "fmt"
18 "io"
19 "mime"
20 "net"
21 "net/http"
22 urlpkg "net/url"
23 "os"
24 "strings"
25 "time"
26
27 "cmd/go/internal/auth"
28 "cmd/go/internal/base"
29 "cmd/go/internal/cfg"
30 "cmd/internal/browser"
31 )
32
33
34
35
36 var impatientInsecureHTTPClient = &http.Client{
37 CheckRedirect: checkRedirect,
38 Timeout: 5 * time.Second,
39 Transport: &http.Transport{
40 Proxy: http.ProxyFromEnvironment,
41 TLSClientConfig: &tls.Config{
42 InsecureSkipVerify: true,
43 },
44 },
45 }
46
47 var securityPreservingDefaultClient = securityPreservingHTTPClient(http.DefaultClient)
48
49
50
51 func securityPreservingHTTPClient(original *http.Client) *http.Client {
52 c := new(http.Client)
53 *c = *original
54 c.CheckRedirect = func(req *http.Request, via []*http.Request) error {
55 if len(via) > 0 && via[0].URL.Scheme == "https" && req.URL.Scheme != "https" {
56 lastHop := via[len(via)-1].URL
57 return fmt.Errorf("redirected from secure URL %s to insecure URL %s", lastHop, req.URL)
58 }
59 return checkRedirect(req, via)
60 }
61 return c
62 }
63
64 func checkRedirect(req *http.Request, via []*http.Request) error {
65
66
67 if len(via) >= 10 {
68 return errors.New("stopped after 10 redirects")
69 }
70
71 interceptRequest(req)
72 return nil
73 }
74
75 type Interceptor struct {
76 Scheme string
77 FromHost string
78 ToHost string
79 Client *http.Client
80 }
81
82 func EnableTestHooks(interceptors []Interceptor) error {
83 if enableTestHooks {
84 return errors.New("web: test hooks already enabled")
85 }
86
87 for _, t := range interceptors {
88 if t.FromHost == "" {
89 panic("EnableTestHooks: missing FromHost")
90 }
91 if t.ToHost == "" {
92 panic("EnableTestHooks: missing ToHost")
93 }
94 }
95
96 testInterceptors = interceptors
97 enableTestHooks = true
98 return nil
99 }
100
101 func DisableTestHooks() {
102 if !enableTestHooks {
103 panic("web: test hooks not enabled")
104 }
105 enableTestHooks = false
106 testInterceptors = nil
107 }
108
109 var (
110 enableTestHooks = false
111 testInterceptors []Interceptor
112 )
113
114 func interceptURL(u *urlpkg.URL) (*Interceptor, bool) {
115 if !enableTestHooks {
116 return nil, false
117 }
118 for i, t := range testInterceptors {
119 if u.Host == t.FromHost && (u.Scheme == "" || u.Scheme == t.Scheme) {
120 return &testInterceptors[i], true
121 }
122 }
123 return nil, false
124 }
125
126 func interceptRequest(req *http.Request) {
127 if t, ok := interceptURL(req.URL); ok {
128 req.Host = req.URL.Host
129 req.URL.Host = t.ToHost
130 }
131 }
132
133 func get(security SecurityMode, url *urlpkg.URL) (*Response, error) {
134 start := time.Now()
135
136 if url.Scheme == "file" {
137 return getFile(url)
138 }
139
140 if enableTestHooks {
141 switch url.Host {
142 case "proxy.golang.org":
143 if os.Getenv("TESTGOPROXY404") == "1" {
144 res := &Response{
145 URL: url.Redacted(),
146 Status: "404 testing",
147 StatusCode: 404,
148 Header: make(map[string][]string),
149 Body: http.NoBody,
150 }
151 if cfg.BuildX {
152 fmt.Fprintf(os.Stderr, "# get %s: %v (%.3fs)\n", url.Redacted(), res.Status, time.Since(start).Seconds())
153 }
154 return res, nil
155 }
156
157 case "localhost.localdev":
158 return nil, fmt.Errorf("no such host localhost.localdev")
159
160 default:
161 if os.Getenv("TESTGONETWORK") == "panic" {
162 if _, ok := interceptURL(url); !ok {
163 host := url.Host
164 if h, _, err := net.SplitHostPort(url.Host); err == nil && h != "" {
165 host = h
166 }
167 addr := net.ParseIP(host)
168 if addr == nil || (!addr.IsLoopback() && !addr.IsUnspecified()) {
169 panic("use of network: " + url.String())
170 }
171 }
172 }
173 }
174 }
175
176 fetch := func(url *urlpkg.URL) (*http.Response, error) {
177
178
179
180
181 if cfg.BuildX {
182 fmt.Fprintf(os.Stderr, "# get %s\n", url.Redacted())
183 }
184
185 req, err := http.NewRequest("GET", url.String(), nil)
186 if err != nil {
187 return nil, err
188 }
189 if url.Scheme == "https" {
190 auth.AddCredentials(req)
191 }
192 t, intercepted := interceptURL(req.URL)
193 if intercepted {
194 req.Host = req.URL.Host
195 req.URL.Host = t.ToHost
196 }
197
198 release, err := base.AcquireNet()
199 if err != nil {
200 return nil, err
201 }
202
203 var res *http.Response
204 if security == Insecure && url.Scheme == "https" {
205 res, err = impatientInsecureHTTPClient.Do(req)
206 } else {
207 if intercepted && t.Client != nil {
208 client := securityPreservingHTTPClient(t.Client)
209 res, err = client.Do(req)
210 } else {
211 res, err = securityPreservingDefaultClient.Do(req)
212 }
213 }
214
215 if err != nil {
216
217
218
219
220 release()
221 return nil, err
222 }
223
224
225
226 body := res.Body
227 res.Body = hookCloser{
228 ReadCloser: body,
229 afterClose: release,
230 }
231 return res, err
232 }
233
234 var (
235 fetched *urlpkg.URL
236 res *http.Response
237 err error
238 )
239 if url.Scheme == "" || url.Scheme == "https" {
240 secure := new(urlpkg.URL)
241 *secure = *url
242 secure.Scheme = "https"
243
244 res, err = fetch(secure)
245 if err == nil {
246 fetched = secure
247 } else {
248 if cfg.BuildX {
249 fmt.Fprintf(os.Stderr, "# get %s: %v\n", secure.Redacted(), err)
250 }
251 if security != Insecure || url.Scheme == "https" {
252
253
254 return nil, err
255 }
256 }
257 }
258
259 if res == nil {
260 switch url.Scheme {
261 case "http":
262 if security == SecureOnly {
263 if cfg.BuildX {
264 fmt.Fprintf(os.Stderr, "# get %s: insecure\n", url.Redacted())
265 }
266 return nil, fmt.Errorf("insecure URL: %s", url.Redacted())
267 }
268 case "":
269 if security != Insecure {
270 panic("should have returned after HTTPS failure")
271 }
272 default:
273 if cfg.BuildX {
274 fmt.Fprintf(os.Stderr, "# get %s: unsupported\n", url.Redacted())
275 }
276 return nil, fmt.Errorf("unsupported scheme: %s", url.Redacted())
277 }
278
279 insecure := new(urlpkg.URL)
280 *insecure = *url
281 insecure.Scheme = "http"
282 if insecure.User != nil && security != Insecure {
283 if cfg.BuildX {
284 fmt.Fprintf(os.Stderr, "# get %s: insecure credentials\n", insecure.Redacted())
285 }
286 return nil, fmt.Errorf("refusing to pass credentials to insecure URL: %s", insecure.Redacted())
287 }
288
289 res, err = fetch(insecure)
290 if err == nil {
291 fetched = insecure
292 } else {
293 if cfg.BuildX {
294 fmt.Fprintf(os.Stderr, "# get %s: %v\n", insecure.Redacted(), err)
295 }
296
297
298 return nil, err
299 }
300 }
301
302
303
304 if cfg.BuildX {
305 fmt.Fprintf(os.Stderr, "# get %s: %v (%.3fs)\n", fetched.Redacted(), res.Status, time.Since(start).Seconds())
306 }
307
308 r := &Response{
309 URL: fetched.Redacted(),
310 Status: res.Status,
311 StatusCode: res.StatusCode,
312 Header: map[string][]string(res.Header),
313 Body: res.Body,
314 }
315
316 if res.StatusCode != http.StatusOK {
317 contentType := res.Header.Get("Content-Type")
318 if mediaType, params, _ := mime.ParseMediaType(contentType); mediaType == "text/plain" {
319 switch charset := strings.ToLower(params["charset"]); charset {
320 case "us-ascii", "utf-8", "":
321
322
323 r.errorDetail.r = res.Body
324 r.Body = &r.errorDetail
325 }
326 }
327 }
328
329 return r, nil
330 }
331
332 func getFile(u *urlpkg.URL) (*Response, error) {
333 path, err := urlToFilePath(u)
334 if err != nil {
335 return nil, err
336 }
337 f, err := os.Open(path)
338
339 if os.IsNotExist(err) {
340 return &Response{
341 URL: u.Redacted(),
342 Status: http.StatusText(http.StatusNotFound),
343 StatusCode: http.StatusNotFound,
344 Body: http.NoBody,
345 fileErr: err,
346 }, nil
347 }
348
349 if os.IsPermission(err) {
350 return &Response{
351 URL: u.Redacted(),
352 Status: http.StatusText(http.StatusForbidden),
353 StatusCode: http.StatusForbidden,
354 Body: http.NoBody,
355 fileErr: err,
356 }, nil
357 }
358
359 if err != nil {
360 return nil, err
361 }
362
363 return &Response{
364 URL: u.Redacted(),
365 Status: http.StatusText(http.StatusOK),
366 StatusCode: http.StatusOK,
367 Body: f,
368 }, nil
369 }
370
371 func openBrowser(url string) bool { return browser.Open(url) }
372
373 func isLocalHost(u *urlpkg.URL) bool {
374
375
376
377 host, _, err := net.SplitHostPort(u.Host)
378 if err != nil {
379 host = u.Host
380 }
381 if host == "localhost" {
382 return true
383 }
384 if ip := net.ParseIP(host); ip != nil && ip.IsLoopback() {
385 return true
386 }
387 return false
388 }
389
390 type hookCloser struct {
391 io.ReadCloser
392 afterClose func()
393 }
394
395 func (c hookCloser) Close() error {
396 err := c.ReadCloser.Close()
397 c.afterClose()
398 return err
399 }
400
View as plain text