| // Copyright 2018 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 web2 |
| |
| import ( |
| "bytes" |
| "cmd/go/internal/base" |
| "encoding/json" |
| "flag" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "net/http" |
| "os" |
| "path/filepath" |
| "runtime" |
| "runtime/debug" |
| "strings" |
| "sync" |
| ) |
| |
| var TraceGET = false |
| var webstack = false |
| |
| func init() { |
| flag.BoolVar(&TraceGET, "webtrace", TraceGET, "trace GET requests") |
| flag.BoolVar(&webstack, "webstack", webstack, "print stack for GET requests") |
| } |
| |
| type netrcLine struct { |
| machine string |
| login string |
| password string |
| } |
| |
| var netrcOnce sync.Once |
| var netrc []netrcLine |
| |
| func parseNetrc(data string) []netrcLine { |
| var nrc []netrcLine |
| var l netrcLine |
| for _, line := range strings.Split(data, "\n") { |
| f := strings.Fields(line) |
| for i := 0; i < len(f)-1; i += 2 { |
| switch f[i] { |
| case "machine": |
| l.machine = f[i+1] |
| case "login": |
| l.login = f[i+1] |
| case "password": |
| l.password = f[i+1] |
| } |
| } |
| if l.machine != "" && l.login != "" && l.password != "" { |
| nrc = append(nrc, l) |
| l = netrcLine{} |
| } |
| } |
| return nrc |
| } |
| |
| func havePassword(machine string) bool { |
| netrcOnce.Do(readNetrc) |
| for _, line := range netrc { |
| if line.machine == machine { |
| return true |
| } |
| } |
| return false |
| } |
| |
| func netrcPath() string { |
| switch runtime.GOOS { |
| case "windows": |
| return filepath.Join(os.Getenv("USERPROFILE"), "_netrc") |
| case "plan9": |
| return filepath.Join(os.Getenv("home"), ".netrc") |
| default: |
| return filepath.Join(os.Getenv("HOME"), ".netrc") |
| } |
| } |
| |
| func readNetrc() { |
| data, err := ioutil.ReadFile(netrcPath()) |
| if err != nil { |
| return |
| } |
| netrc = parseNetrc(string(data)) |
| } |
| |
| type getState struct { |
| req *http.Request |
| resp *http.Response |
| body io.ReadCloser |
| non200ok bool |
| } |
| |
| type Option interface { |
| option(*getState) error |
| } |
| |
| func Non200OK() Option { |
| return optionFunc(func(g *getState) error { |
| g.non200ok = true |
| return nil |
| }) |
| } |
| |
| type optionFunc func(*getState) error |
| |
| func (f optionFunc) option(g *getState) error { |
| return f(g) |
| } |
| |
| func DecodeJSON(dst interface{}) Option { |
| return optionFunc(func(g *getState) error { |
| if g.resp != nil { |
| return json.NewDecoder(g.body).Decode(dst) |
| } |
| return nil |
| }) |
| } |
| |
| func ReadAllBody(body *[]byte) Option { |
| return optionFunc(func(g *getState) error { |
| if g.resp != nil { |
| var err error |
| *body, err = ioutil.ReadAll(g.body) |
| return err |
| } |
| return nil |
| }) |
| } |
| |
| func Body(body *io.ReadCloser) Option { |
| return optionFunc(func(g *getState) error { |
| if g.resp != nil { |
| *body = g.body |
| g.body = nil |
| } |
| return nil |
| }) |
| } |
| |
| func Header(hdr *http.Header) Option { |
| return optionFunc(func(g *getState) error { |
| if g.resp != nil { |
| *hdr = CopyHeader(g.resp.Header) |
| } |
| return nil |
| }) |
| } |
| |
| func CopyHeader(hdr http.Header) http.Header { |
| if hdr == nil { |
| return nil |
| } |
| h2 := make(http.Header) |
| for k, v := range hdr { |
| v2 := make([]string, len(v)) |
| copy(v2, v) |
| h2[k] = v2 |
| } |
| return h2 |
| } |
| |
| var cache struct { |
| mu sync.Mutex |
| byURL map[string]*cacheEntry |
| } |
| |
| type cacheEntry struct { |
| mu sync.Mutex |
| resp *http.Response |
| body []byte |
| } |
| |
| var httpDo = http.DefaultClient.Do |
| |
| func SetHTTPDoForTesting(do func(*http.Request) (*http.Response, error)) { |
| if do == nil { |
| do = http.DefaultClient.Do |
| } |
| httpDo = do |
| } |
| |
| func Get(url string, options ...Option) error { |
| if TraceGET || webstack { |
| println("GET", url) |
| if webstack { |
| println(string(debug.Stack())) |
| } |
| } |
| |
| req, err := http.NewRequest("GET", url, nil) |
| if err != nil { |
| return err |
| } |
| |
| netrcOnce.Do(readNetrc) |
| for _, l := range netrc { |
| if l.machine == req.URL.Host { |
| req.SetBasicAuth(l.login, l.password) |
| break |
| } |
| } |
| |
| g := &getState{req: req} |
| for _, o := range options { |
| if err := o.option(g); err != nil { |
| return err |
| } |
| } |
| |
| cache.mu.Lock() |
| e := cache.byURL[url] |
| if e == nil { |
| e = new(cacheEntry) |
| if !strings.HasPrefix(url, "file:") { |
| if cache.byURL == nil { |
| cache.byURL = make(map[string]*cacheEntry) |
| } |
| cache.byURL[url] = e |
| } |
| } |
| cache.mu.Unlock() |
| |
| e.mu.Lock() |
| if strings.HasPrefix(url, "file:") { |
| body, err := ioutil.ReadFile(req.URL.Path) |
| if err != nil { |
| e.mu.Unlock() |
| return err |
| } |
| e.body = body |
| e.resp = &http.Response{ |
| StatusCode: 200, |
| } |
| } else if e.resp == nil { |
| resp, err := httpDo(req) |
| if err != nil { |
| e.mu.Unlock() |
| return err |
| } |
| e.resp = resp |
| // TODO: Spool to temp file. |
| body, err := ioutil.ReadAll(resp.Body) |
| resp.Body.Close() |
| resp.Body = nil |
| if err != nil { |
| e.mu.Unlock() |
| return err |
| } |
| e.body = body |
| } |
| g.resp = e.resp |
| g.body = ioutil.NopCloser(bytes.NewReader(e.body)) |
| e.mu.Unlock() |
| |
| defer func() { |
| if g.body != nil { |
| g.body.Close() |
| } |
| }() |
| |
| if g.resp.StatusCode == 403 && req.URL.Host == "api.github.com" && !havePassword("api.github.com") { |
| base.Errorf("%s", githubMessage) |
| } |
| if !g.non200ok && g.resp.StatusCode != 200 { |
| return fmt.Errorf("unexpected status (%s): %v", url, g.resp.Status) |
| } |
| |
| for _, o := range options { |
| if err := o.option(g); err != nil { |
| return err |
| } |
| } |
| return err |
| } |
| |
| var githubMessage = `vgo: 403 response from api.github.com |
| |
| GitHub applies fairly small rate limits to unauthenticated users, and |
| you appear to be hitting them. To authenticate, please visit |
| https://github.com/settings/tokens and click "Generate New Token" to |
| create a Personal Access Token. The token only needs "public_repo" |
| scope, but you can add "repo" if you want to access private |
| repositories too. |
| |
| Add the token to your $HOME/.netrc (%USERPROFILE%\_netrc on Windows): |
| |
| machine api.github.com login YOU password TOKEN |
| |
| Sorry for the interruption. |
| ` |