github.com/sirkon/goproxy@v1.4.8/internal/web2/web.go (about)

     1  // Copyright 2018 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 web2
     6  
     7  import (
     8  	"bytes"
     9  	"github.com/sirkon/goproxy/internal/base"
    10  	"encoding/json"
    11  	"flag"
    12  	"fmt"
    13  	"io"
    14  	"io/ioutil"
    15  	"net/http"
    16  	"os"
    17  	"path/filepath"
    18  	"runtime"
    19  	"runtime/debug"
    20  	"strings"
    21  	"sync"
    22  )
    23  
    24  var TraceGET = false
    25  var webstack = false
    26  
    27  func init() {
    28  	flag.BoolVar(&TraceGET, "webtrace", TraceGET, "trace GET requests")
    29  	flag.BoolVar(&webstack, "webstack", webstack, "print stack for GET requests")
    30  }
    31  
    32  type netrcLine struct {
    33  	machine  string
    34  	login    string
    35  	password string
    36  }
    37  
    38  var netrcOnce sync.Once
    39  var netrc []netrcLine
    40  
    41  func parseNetrc(data string) []netrcLine {
    42  	var nrc []netrcLine
    43  	var l netrcLine
    44  	for _, line := range strings.Split(data, "\n") {
    45  		f := strings.Fields(line)
    46  		for i := 0; i < len(f)-1; i += 2 {
    47  			switch f[i] {
    48  			case "machine":
    49  				l.machine = f[i+1]
    50  			case "login":
    51  				l.login = f[i+1]
    52  			case "password":
    53  				l.password = f[i+1]
    54  			}
    55  		}
    56  		if l.machine != "" && l.login != "" && l.password != "" {
    57  			nrc = append(nrc, l)
    58  			l = netrcLine{}
    59  		}
    60  	}
    61  	return nrc
    62  }
    63  
    64  func havePassword(machine string) bool {
    65  	netrcOnce.Do(readNetrc)
    66  	for _, line := range netrc {
    67  		if line.machine == machine {
    68  			return true
    69  		}
    70  	}
    71  	return false
    72  }
    73  
    74  func netrcPath() string {
    75  	switch runtime.GOOS {
    76  	case "windows":
    77  		return filepath.Join(os.Getenv("USERPROFILE"), "_netrc")
    78  	case "plan9":
    79  		return filepath.Join(os.Getenv("home"), ".netrc")
    80  	default:
    81  		return filepath.Join(os.Getenv("HOME"), ".netrc")
    82  	}
    83  }
    84  
    85  func readNetrc() {
    86  	data, err := ioutil.ReadFile(netrcPath())
    87  	if err != nil {
    88  		return
    89  	}
    90  	netrc = parseNetrc(string(data))
    91  }
    92  
    93  type getState struct {
    94  	req      *http.Request
    95  	resp     *http.Response
    96  	body     io.ReadCloser
    97  	non200ok bool
    98  }
    99  
   100  type Option interface {
   101  	option(*getState) error
   102  }
   103  
   104  func Non200OK() Option {
   105  	return optionFunc(func(g *getState) error {
   106  		g.non200ok = true
   107  		return nil
   108  	})
   109  }
   110  
   111  type optionFunc func(*getState) error
   112  
   113  func (f optionFunc) option(g *getState) error {
   114  	return f(g)
   115  }
   116  
   117  func DecodeJSON(dst interface{}) Option {
   118  	return optionFunc(func(g *getState) error {
   119  		if g.resp != nil {
   120  			return json.NewDecoder(g.body).Decode(dst)
   121  		}
   122  		return nil
   123  	})
   124  }
   125  
   126  func ReadAllBody(body *[]byte) Option {
   127  	return optionFunc(func(g *getState) error {
   128  		if g.resp != nil {
   129  			var err error
   130  			*body, err = ioutil.ReadAll(g.body)
   131  			return err
   132  		}
   133  		return nil
   134  	})
   135  }
   136  
   137  func Body(body *io.ReadCloser) Option {
   138  	return optionFunc(func(g *getState) error {
   139  		if g.resp != nil {
   140  			*body = g.body
   141  			g.body = nil
   142  		}
   143  		return nil
   144  	})
   145  }
   146  
   147  func Header(hdr *http.Header) Option {
   148  	return optionFunc(func(g *getState) error {
   149  		if g.resp != nil {
   150  			*hdr = CopyHeader(g.resp.Header)
   151  		}
   152  		return nil
   153  	})
   154  }
   155  
   156  func CopyHeader(hdr http.Header) http.Header {
   157  	if hdr == nil {
   158  		return nil
   159  	}
   160  	h2 := make(http.Header)
   161  	for k, v := range hdr {
   162  		v2 := make([]string, len(v))
   163  		copy(v2, v)
   164  		h2[k] = v2
   165  	}
   166  	return h2
   167  }
   168  
   169  var cache struct {
   170  	mu    sync.Mutex
   171  	byURL map[string]*cacheEntry
   172  }
   173  
   174  type cacheEntry struct {
   175  	mu   sync.Mutex
   176  	resp *http.Response
   177  	body []byte
   178  }
   179  
   180  var httpDo = http.DefaultClient.Do
   181  
   182  func SetHTTPDoForTesting(do func(*http.Request) (*http.Response, error)) {
   183  	if do == nil {
   184  		do = http.DefaultClient.Do
   185  	}
   186  	httpDo = do
   187  }
   188  
   189  func Get(url string, options ...Option) error {
   190  	if TraceGET || webstack {
   191  		println("GET", url)
   192  		if webstack {
   193  			println(string(debug.Stack()))
   194  		}
   195  	}
   196  
   197  	req, err := http.NewRequest("GET", url, nil)
   198  	if err != nil {
   199  		return err
   200  	}
   201  
   202  	netrcOnce.Do(readNetrc)
   203  	for _, l := range netrc {
   204  		if l.machine == req.URL.Host {
   205  			req.SetBasicAuth(l.login, l.password)
   206  			break
   207  		}
   208  	}
   209  
   210  	g := &getState{req: req}
   211  	for _, o := range options {
   212  		if err := o.option(g); err != nil {
   213  			return err
   214  		}
   215  	}
   216  
   217  	cache.mu.Lock()
   218  	e := cache.byURL[url]
   219  	if e == nil {
   220  		e = new(cacheEntry)
   221  		if !strings.HasPrefix(url, "file:") {
   222  			if cache.byURL == nil {
   223  				cache.byURL = make(map[string]*cacheEntry)
   224  			}
   225  			cache.byURL[url] = e
   226  		}
   227  	}
   228  	cache.mu.Unlock()
   229  
   230  	e.mu.Lock()
   231  	if strings.HasPrefix(url, "file:") {
   232  		body, err := ioutil.ReadFile(req.URL.Path)
   233  		if err != nil {
   234  			e.mu.Unlock()
   235  			return err
   236  		}
   237  		e.body = body
   238  		e.resp = &http.Response{
   239  			StatusCode: 200,
   240  		}
   241  	} else if e.resp == nil {
   242  		resp, err := httpDo(req)
   243  		if err != nil {
   244  			e.mu.Unlock()
   245  			return err
   246  		}
   247  		e.resp = resp
   248  		// TODO: Spool to temp file.
   249  		body, err := ioutil.ReadAll(resp.Body)
   250  		resp.Body.Close()
   251  		resp.Body = nil
   252  		if err != nil {
   253  			e.mu.Unlock()
   254  			return err
   255  		}
   256  		e.body = body
   257  	}
   258  	g.resp = e.resp
   259  	g.body = ioutil.NopCloser(bytes.NewReader(e.body))
   260  	e.mu.Unlock()
   261  
   262  	defer func() {
   263  		if g.body != nil {
   264  			g.body.Close()
   265  		}
   266  	}()
   267  
   268  	if g.resp.StatusCode == 403 && req.URL.Host == "api.github.com" && !havePassword("api.github.com") {
   269  		base.Errorf("%s", githubMessage)
   270  	}
   271  	if !g.non200ok && g.resp.StatusCode != 200 {
   272  		return fmt.Errorf("unexpected status (%s): %v", url, g.resp.Status)
   273  	}
   274  
   275  	for _, o := range options {
   276  		if err := o.option(g); err != nil {
   277  			return err
   278  		}
   279  	}
   280  	return err
   281  }
   282  
   283  var githubMessage = `go: 403 response from api.github.com
   284  
   285  GitHub applies fairly small rate limits to unauthenticated users, and
   286  you appear to be hitting them. To authenticate, please visit
   287  https://github.com/settings/tokens and click "Generate New Token" to
   288  create a Personal Access Token. The token only needs "public_repo"
   289  scope, but you can add "repo" if you want to access private
   290  repositories too.
   291  
   292  Add the token to your $HOME/.netrc (%USERPROFILE%\_netrc on Windows):
   293  
   294      machine api.github.com login YOU password TOKEN
   295  
   296  Sorry for the interruption.
   297  `