github.com/webx-top/com@v1.2.12/http.go (about) 1 // Copyright 2013 com authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 // not use this file except in compliance with the License. You may obtain 5 // a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 // License for the specific language governing permissions and limitations 13 // under the License. 14 15 package com 16 17 import ( 18 "bytes" 19 "context" 20 "encoding/json" 21 "errors" 22 "fmt" 23 "io" 24 "log" 25 "net" 26 "net/http" 27 "net/url" 28 "os" 29 "path/filepath" 30 "strings" 31 "time" 32 ) 33 34 type NotFoundError struct { 35 Message string 36 } 37 38 func (e NotFoundError) Error() string { 39 return e.Message 40 } 41 42 type RemoteError struct { 43 Host string 44 Err error 45 } 46 47 func (e *RemoteError) Error() string { 48 return e.Err.Error() 49 } 50 51 var UserAgent = "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1541.0 Safari/537.36" 52 53 // HTTPGet gets the specified resource. ErrNotFound is returned if the 54 // server responds with status 404. 55 func HTTPGet(client *http.Client, url string, header http.Header) (io.ReadCloser, error) { 56 req, err := http.NewRequest("GET", url, nil) 57 if err != nil { 58 return nil, err 59 } 60 req.Header.Set("User-Agent", UserAgent) 61 for k, vs := range header { 62 req.Header[k] = vs 63 } 64 resp, err := client.Do(req) 65 if err != nil { 66 return nil, &RemoteError{req.URL.Host, err} 67 } 68 if resp.StatusCode == 200 { 69 return resp.Body, nil 70 } 71 resp.Body.Close() 72 if resp.StatusCode == 404 { // 403 can be rate limit error. || resp.StatusCode == 403 { 73 err = NotFoundError{"Resource not found: " + url} 74 } else { 75 err = &RemoteError{req.URL.Host, fmt.Errorf("get %s -> %d", url, resp.StatusCode)} 76 } 77 return nil, err 78 } 79 80 // HTTPGetToFile gets the specified resource and writes to file. 81 // ErrNotFound is returned if the server responds with status 404. 82 func HTTPGetToFile(client *http.Client, url string, header http.Header, fileName string) error { 83 rc, err := HTTPGet(client, url, header) 84 if err != nil { 85 return err 86 } 87 defer rc.Close() 88 89 os.MkdirAll(filepath.Dir(fileName), os.ModePerm) 90 f, err := os.Create(fileName) 91 if err != nil { 92 return err 93 } 94 defer f.Close() 95 _, err = io.Copy(f, rc) 96 if err != nil { 97 return err 98 } 99 err = f.Sync() 100 return err 101 } 102 103 // HTTPGetBytes gets the specified resource. ErrNotFound is returned if the server 104 // responds with status 404. 105 func HTTPGetBytes(client *http.Client, url string, header http.Header) ([]byte, error) { 106 rc, err := HTTPGet(client, url, header) 107 if err != nil { 108 return nil, err 109 } 110 defer rc.Close() 111 return io.ReadAll(rc) 112 } 113 114 // HTTPGetJSON gets the specified resource and mapping to struct. 115 // ErrNotFound is returned if the server responds with status 404. 116 func HTTPGetJSON(client *http.Client, url string, v interface{}) error { 117 rc, err := HTTPGet(client, url, nil) 118 if err != nil { 119 return err 120 } 121 defer rc.Close() 122 err = json.NewDecoder(rc).Decode(v) 123 if _, ok := err.(*json.SyntaxError); ok { 124 err = NotFoundError{"JSON syntax error at " + url} 125 } 126 return err 127 } 128 129 // A RawFile describes a file that can be downloaded. 130 type RawFile interface { 131 Name() string 132 RawUrl() string 133 Data() []byte 134 SetData([]byte) 135 } 136 137 // FetchFiles fetches files specified by the rawURL field in parallel. 138 func FetchFiles(client *http.Client, files []RawFile, header http.Header) error { 139 ch := make(chan error, len(files)) 140 for i := range files { 141 go func(i int) { 142 p, err := HTTPGetBytes(client, files[i].RawUrl(), nil) 143 if err != nil { 144 ch <- err 145 return 146 } 147 files[i].SetData(p) 148 ch <- nil 149 }(i) 150 } 151 for range files { 152 if err := <-ch; err != nil { 153 return err 154 } 155 } 156 return nil 157 } 158 159 // FetchFilesCurl uses command `curl` to fetch files specified by the rawURL field in parallel. 160 func FetchFilesCurl(files []RawFile, curlOptions ...string) error { 161 ch := make(chan error, len(files)) 162 for i := range files { 163 go func(i int) { 164 stdout, _, err := ExecCmd("curl", append(curlOptions, files[i].RawUrl())...) 165 if err != nil { 166 ch <- err 167 return 168 } 169 170 files[i].SetData([]byte(stdout)) 171 ch <- nil 172 }(i) 173 } 174 for range files { 175 if err := <-ch; err != nil { 176 return err 177 } 178 } 179 return nil 180 } 181 182 // HTTPPost ============================== 183 func HTTPPost(client *http.Client, url string, body []byte, header http.Header) (io.ReadCloser, error) { 184 req, err := http.NewRequest("POST", url, bytes.NewReader(body)) 185 if err != nil { 186 return nil, err 187 } 188 req.Header.Set("User-Agent", UserAgent) 189 for k, vs := range header { 190 req.Header[k] = vs 191 } 192 resp, err := client.Do(req) 193 if err != nil { 194 return nil, &RemoteError{req.URL.Host, err} 195 } 196 if resp.StatusCode == 200 { 197 return resp.Body, nil 198 } 199 resp.Body.Close() 200 if resp.StatusCode == 404 { // 403 can be rate limit error. || resp.StatusCode == 403 { 201 err = NotFoundError{"Resource not found: " + url} 202 } else { 203 err = &RemoteError{req.URL.Host, fmt.Errorf("get %s -> %d", url, resp.StatusCode)} 204 } 205 return nil, err 206 } 207 208 func HTTPPostBytes(client *http.Client, url string, body []byte, header http.Header) ([]byte, error) { 209 rc, err := HTTPPost(client, url, body, header) 210 if err != nil { 211 return nil, err 212 } 213 p, err := io.ReadAll(rc) 214 rc.Close() 215 return p, err 216 } 217 218 func HTTPPostJSON(client *http.Client, url string, body []byte, header http.Header) ([]byte, error) { 219 if header == nil { 220 header = http.Header{} 221 } 222 header.Add("Content-Type", "application/json") 223 p, err := HTTPPostBytes(client, url, body, header) 224 if err != nil { 225 return []byte{}, err 226 } 227 return p, nil 228 } 229 230 // NewCookie is a helper method that returns a new http.Cookie object. 231 // Duration is specified in seconds. If the duration is zero, the cookie is permanent. 232 // This can be used in conjunction with ctx.SetCookie. 233 func NewCookie(name string, value string, args ...interface{}) *http.Cookie { 234 var ( 235 alen = len(args) 236 age int64 237 path string 238 domain string 239 secure bool 240 httpOnly bool 241 ) 242 switch alen { 243 case 5: 244 httpOnly, _ = args[4].(bool) 245 fallthrough 246 case 4: 247 secure, _ = args[3].(bool) 248 fallthrough 249 case 3: 250 domain, _ = args[2].(string) 251 fallthrough 252 case 2: 253 path, _ = args[1].(string) 254 fallthrough 255 case 1: 256 switch args[0].(type) { 257 case int: 258 age = int64(args[0].(int)) 259 case int64: 260 age = args[0].(int64) 261 case time.Duration: 262 age = int64(args[0].(time.Duration)) 263 } 264 } 265 cookie := &http.Cookie{ 266 Name: name, 267 Value: value, 268 Path: path, 269 Domain: domain, 270 MaxAge: 0, 271 Secure: secure, 272 HttpOnly: httpOnly, 273 } 274 if age > 0 { 275 cookie.Expires = time.Unix(time.Now().Unix()+age, 0) 276 } else if age < 0 { 277 cookie.Expires = time.Unix(1, 0) 278 } 279 return cookie 280 } 281 282 type HTTPClientOptions func(c *http.Client) 283 284 func HTTPClientWithTimeout(timeout time.Duration, options ...HTTPClientOptions) *http.Client { 285 client := &http.Client{ 286 Transport: &http.Transport{ 287 Dial: func(netw, addr string) (net.Conn, error) { 288 conn, err := net.DialTimeout(netw, addr, timeout) 289 if err != nil { 290 return nil, err 291 } 292 conn.SetDeadline(time.Now().Add(timeout)) 293 return conn, nil 294 }, 295 ResponseHeaderTimeout: timeout, 296 }, 297 } 298 for _, opt := range options { 299 opt(client) 300 } 301 return client 302 } 303 304 // IsNetworkOrHostDown - if there was a network error or if the host is down. 305 // expectTimeouts indicates that *context* timeouts are expected and does not 306 // indicate a downed host. Other timeouts still returns down. 307 func IsNetworkOrHostDown(err error, expectTimeouts bool) bool { 308 if err == nil { 309 return false 310 } 311 312 if errors.Is(err, context.Canceled) { 313 return false 314 } 315 316 if errors.Is(err, context.DeadlineExceeded) { 317 return !expectTimeouts 318 } 319 320 // We need to figure if the error either a timeout 321 // or a non-temporary error. 322 var urlErr *url.Error 323 if errors.As(err, &urlErr) { 324 switch urlErr.Err.(type) { 325 case *net.DNSError, *net.OpError, net.UnknownNetworkError: 326 return true 327 } 328 } 329 var e net.Error 330 if errors.As(err, &e) { 331 if e.Timeout() { 332 return true 333 } 334 } 335 336 // Fallback to other mechanisms. 337 switch { 338 case strings.Contains(err.Error(), "Connection closed by foreign host"): 339 return true 340 case strings.Contains(err.Error(), "TLS handshake timeout"): 341 // If error is - tlsHandshakeTimeoutError. 342 return true 343 case strings.Contains(err.Error(), "i/o timeout"): 344 // If error is - tcp timeoutError. 345 return true 346 case strings.Contains(err.Error(), "connection timed out"): 347 // If err is a net.Dial timeout. 348 return true 349 case strings.Contains(err.Error(), "connection refused"): 350 // If err is connection refused 351 return true 352 353 case strings.Contains(strings.ToLower(err.Error()), "503 service unavailable"): 354 // Denial errors 355 return true 356 } 357 return false 358 } 359 360 func HTTPCanRetry(code int) bool { 361 return code < 200 || (code > 299 && code < http.StatusInternalServerError) 362 } 363 364 func ParseHTTPRetryAfter(res http.ResponseWriter) time.Duration { 365 r := res.Header().Get(`Retry-After`) 366 return ParseRetryAfter(r) 367 } 368 369 func ParseRetryAfter(r string) time.Duration { 370 if len(r) == 0 { 371 return 0 372 } 373 if StrIsNumeric(r) { 374 i := Int64(r) 375 if i <= 0 { 376 return 0 377 } 378 return time.Duration(i) * time.Second 379 } 380 t, err := time.Parse(time.RFC1123, r) 381 if err != nil { 382 log.Printf("failed to ParseRetryAfter(%q): %v\n", r, err) 383 return 0 384 } 385 //fmt.Printf("%+v", t.String()) 386 if t.Before(time.Now()) { 387 return 0 388 } 389 return time.Until(t) 390 }