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  }