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 `