github.com/astaxie/beego@v1.12.3/httplib/httplib.go (about)

     1  // Copyright 2014 beego Author. All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain 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,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package httplib is used as http.Client
    16  // Usage:
    17  //
    18  // import "github.com/astaxie/beego/httplib"
    19  //
    20  //	b := httplib.Post("http://beego.me/")
    21  //	b.Param("username","astaxie")
    22  //	b.Param("password","123456")
    23  //	b.PostFile("uploadfile1", "httplib.pdf")
    24  //	b.PostFile("uploadfile2", "httplib.txt")
    25  //	str, err := b.String()
    26  //	if err != nil {
    27  //		t.Fatal(err)
    28  //	}
    29  //	fmt.Println(str)
    30  //
    31  //  more docs http://beego.me/docs/module/httplib.md
    32  package httplib
    33  
    34  import (
    35  	"bytes"
    36  	"compress/gzip"
    37  	"crypto/tls"
    38  	"encoding/json"
    39  	"encoding/xml"
    40  	"io"
    41  	"io/ioutil"
    42  	"log"
    43  	"mime/multipart"
    44  	"net"
    45  	"net/http"
    46  	"net/http/cookiejar"
    47  	"net/http/httputil"
    48  	"net/url"
    49  	"os"
    50  	"path"
    51  	"strings"
    52  	"sync"
    53  	"time"
    54  
    55  	"gopkg.in/yaml.v2"
    56  )
    57  
    58  var defaultSetting = BeegoHTTPSettings{
    59  	UserAgent:        "beegoServer",
    60  	ConnectTimeout:   60 * time.Second,
    61  	ReadWriteTimeout: 60 * time.Second,
    62  	Gzip:             true,
    63  	DumpBody:         true,
    64  }
    65  
    66  var defaultCookieJar http.CookieJar
    67  var settingMutex sync.Mutex
    68  
    69  // createDefaultCookie creates a global cookiejar to store cookies.
    70  func createDefaultCookie() {
    71  	settingMutex.Lock()
    72  	defer settingMutex.Unlock()
    73  	defaultCookieJar, _ = cookiejar.New(nil)
    74  }
    75  
    76  // SetDefaultSetting Overwrite default settings
    77  func SetDefaultSetting(setting BeegoHTTPSettings) {
    78  	settingMutex.Lock()
    79  	defer settingMutex.Unlock()
    80  	defaultSetting = setting
    81  }
    82  
    83  // NewBeegoRequest return *BeegoHttpRequest with specific method
    84  func NewBeegoRequest(rawurl, method string) *BeegoHTTPRequest {
    85  	var resp http.Response
    86  	u, err := url.Parse(rawurl)
    87  	if err != nil {
    88  		log.Println("Httplib:", err)
    89  	}
    90  	req := http.Request{
    91  		URL:        u,
    92  		Method:     method,
    93  		Header:     make(http.Header),
    94  		Proto:      "HTTP/1.1",
    95  		ProtoMajor: 1,
    96  		ProtoMinor: 1,
    97  	}
    98  	return &BeegoHTTPRequest{
    99  		url:     rawurl,
   100  		req:     &req,
   101  		params:  map[string][]string{},
   102  		files:   map[string]string{},
   103  		setting: defaultSetting,
   104  		resp:    &resp,
   105  	}
   106  }
   107  
   108  // Get returns *BeegoHttpRequest with GET method.
   109  func Get(url string) *BeegoHTTPRequest {
   110  	return NewBeegoRequest(url, "GET")
   111  }
   112  
   113  // Post returns *BeegoHttpRequest with POST method.
   114  func Post(url string) *BeegoHTTPRequest {
   115  	return NewBeegoRequest(url, "POST")
   116  }
   117  
   118  // Put returns *BeegoHttpRequest with PUT method.
   119  func Put(url string) *BeegoHTTPRequest {
   120  	return NewBeegoRequest(url, "PUT")
   121  }
   122  
   123  // Delete returns *BeegoHttpRequest DELETE method.
   124  func Delete(url string) *BeegoHTTPRequest {
   125  	return NewBeegoRequest(url, "DELETE")
   126  }
   127  
   128  // Head returns *BeegoHttpRequest with HEAD method.
   129  func Head(url string) *BeegoHTTPRequest {
   130  	return NewBeegoRequest(url, "HEAD")
   131  }
   132  
   133  // BeegoHTTPSettings is the http.Client setting
   134  type BeegoHTTPSettings struct {
   135  	ShowDebug        bool
   136  	UserAgent        string
   137  	ConnectTimeout   time.Duration
   138  	ReadWriteTimeout time.Duration
   139  	TLSClientConfig  *tls.Config
   140  	Proxy            func(*http.Request) (*url.URL, error)
   141  	Transport        http.RoundTripper
   142  	CheckRedirect    func(req *http.Request, via []*http.Request) error
   143  	EnableCookie     bool
   144  	Gzip             bool
   145  	DumpBody         bool
   146  	Retries          int // if set to -1 means will retry forever
   147  	RetryDelay       time.Duration
   148  }
   149  
   150  // BeegoHTTPRequest provides more useful methods for requesting one url than http.Request.
   151  type BeegoHTTPRequest struct {
   152  	url     string
   153  	req     *http.Request
   154  	params  map[string][]string
   155  	files   map[string]string
   156  	setting BeegoHTTPSettings
   157  	resp    *http.Response
   158  	body    []byte
   159  	dump    []byte
   160  }
   161  
   162  // GetRequest return the request object
   163  func (b *BeegoHTTPRequest) GetRequest() *http.Request {
   164  	return b.req
   165  }
   166  
   167  // Setting Change request settings
   168  func (b *BeegoHTTPRequest) Setting(setting BeegoHTTPSettings) *BeegoHTTPRequest {
   169  	b.setting = setting
   170  	return b
   171  }
   172  
   173  // SetBasicAuth sets the request's Authorization header to use HTTP Basic Authentication with the provided username and password.
   174  func (b *BeegoHTTPRequest) SetBasicAuth(username, password string) *BeegoHTTPRequest {
   175  	b.req.SetBasicAuth(username, password)
   176  	return b
   177  }
   178  
   179  // SetEnableCookie sets enable/disable cookiejar
   180  func (b *BeegoHTTPRequest) SetEnableCookie(enable bool) *BeegoHTTPRequest {
   181  	b.setting.EnableCookie = enable
   182  	return b
   183  }
   184  
   185  // SetUserAgent sets User-Agent header field
   186  func (b *BeegoHTTPRequest) SetUserAgent(useragent string) *BeegoHTTPRequest {
   187  	b.setting.UserAgent = useragent
   188  	return b
   189  }
   190  
   191  // Debug sets show debug or not when executing request.
   192  func (b *BeegoHTTPRequest) Debug(isdebug bool) *BeegoHTTPRequest {
   193  	b.setting.ShowDebug = isdebug
   194  	return b
   195  }
   196  
   197  // Retries sets Retries times.
   198  // default is 0 means no retried.
   199  // -1 means retried forever.
   200  // others means retried times.
   201  func (b *BeegoHTTPRequest) Retries(times int) *BeegoHTTPRequest {
   202  	b.setting.Retries = times
   203  	return b
   204  }
   205  
   206  func (b *BeegoHTTPRequest) RetryDelay(delay time.Duration) *BeegoHTTPRequest {
   207  	b.setting.RetryDelay = delay
   208  	return b
   209  }
   210  
   211  // DumpBody setting whether need to Dump the Body.
   212  func (b *BeegoHTTPRequest) DumpBody(isdump bool) *BeegoHTTPRequest {
   213  	b.setting.DumpBody = isdump
   214  	return b
   215  }
   216  
   217  // DumpRequest return the DumpRequest
   218  func (b *BeegoHTTPRequest) DumpRequest() []byte {
   219  	return b.dump
   220  }
   221  
   222  // SetTimeout sets connect time out and read-write time out for BeegoRequest.
   223  func (b *BeegoHTTPRequest) SetTimeout(connectTimeout, readWriteTimeout time.Duration) *BeegoHTTPRequest {
   224  	b.setting.ConnectTimeout = connectTimeout
   225  	b.setting.ReadWriteTimeout = readWriteTimeout
   226  	return b
   227  }
   228  
   229  // SetTLSClientConfig sets tls connection configurations if visiting https url.
   230  func (b *BeegoHTTPRequest) SetTLSClientConfig(config *tls.Config) *BeegoHTTPRequest {
   231  	b.setting.TLSClientConfig = config
   232  	return b
   233  }
   234  
   235  // Header add header item string in request.
   236  func (b *BeegoHTTPRequest) Header(key, value string) *BeegoHTTPRequest {
   237  	b.req.Header.Set(key, value)
   238  	return b
   239  }
   240  
   241  // SetHost set the request host
   242  func (b *BeegoHTTPRequest) SetHost(host string) *BeegoHTTPRequest {
   243  	b.req.Host = host
   244  	return b
   245  }
   246  
   247  // SetProtocolVersion Set the protocol version for incoming requests.
   248  // Client requests always use HTTP/1.1.
   249  func (b *BeegoHTTPRequest) SetProtocolVersion(vers string) *BeegoHTTPRequest {
   250  	if len(vers) == 0 {
   251  		vers = "HTTP/1.1"
   252  	}
   253  
   254  	major, minor, ok := http.ParseHTTPVersion(vers)
   255  	if ok {
   256  		b.req.Proto = vers
   257  		b.req.ProtoMajor = major
   258  		b.req.ProtoMinor = minor
   259  	}
   260  
   261  	return b
   262  }
   263  
   264  // SetCookie add cookie into request.
   265  func (b *BeegoHTTPRequest) SetCookie(cookie *http.Cookie) *BeegoHTTPRequest {
   266  	b.req.Header.Add("Cookie", cookie.String())
   267  	return b
   268  }
   269  
   270  // SetTransport set the setting transport
   271  func (b *BeegoHTTPRequest) SetTransport(transport http.RoundTripper) *BeegoHTTPRequest {
   272  	b.setting.Transport = transport
   273  	return b
   274  }
   275  
   276  // SetProxy set the http proxy
   277  // example:
   278  //
   279  //	func(req *http.Request) (*url.URL, error) {
   280  // 		u, _ := url.ParseRequestURI("http://127.0.0.1:8118")
   281  // 		return u, nil
   282  // 	}
   283  func (b *BeegoHTTPRequest) SetProxy(proxy func(*http.Request) (*url.URL, error)) *BeegoHTTPRequest {
   284  	b.setting.Proxy = proxy
   285  	return b
   286  }
   287  
   288  // SetCheckRedirect specifies the policy for handling redirects.
   289  //
   290  // If CheckRedirect is nil, the Client uses its default policy,
   291  // which is to stop after 10 consecutive requests.
   292  func (b *BeegoHTTPRequest) SetCheckRedirect(redirect func(req *http.Request, via []*http.Request) error) *BeegoHTTPRequest {
   293  	b.setting.CheckRedirect = redirect
   294  	return b
   295  }
   296  
   297  // Param adds query param in to request.
   298  // params build query string as ?key1=value1&key2=value2...
   299  func (b *BeegoHTTPRequest) Param(key, value string) *BeegoHTTPRequest {
   300  	if param, ok := b.params[key]; ok {
   301  		b.params[key] = append(param, value)
   302  	} else {
   303  		b.params[key] = []string{value}
   304  	}
   305  	return b
   306  }
   307  
   308  // PostFile add a post file to the request
   309  func (b *BeegoHTTPRequest) PostFile(formname, filename string) *BeegoHTTPRequest {
   310  	b.files[formname] = filename
   311  	return b
   312  }
   313  
   314  // Body adds request raw body.
   315  // it supports string and []byte.
   316  func (b *BeegoHTTPRequest) Body(data interface{}) *BeegoHTTPRequest {
   317  	switch t := data.(type) {
   318  	case string:
   319  		bf := bytes.NewBufferString(t)
   320  		b.req.Body = ioutil.NopCloser(bf)
   321  		b.req.ContentLength = int64(len(t))
   322  	case []byte:
   323  		bf := bytes.NewBuffer(t)
   324  		b.req.Body = ioutil.NopCloser(bf)
   325  		b.req.ContentLength = int64(len(t))
   326  	}
   327  	return b
   328  }
   329  
   330  // XMLBody adds request raw body encoding by XML.
   331  func (b *BeegoHTTPRequest) XMLBody(obj interface{}) (*BeegoHTTPRequest, error) {
   332  	if b.req.Body == nil && obj != nil {
   333  		byts, err := xml.Marshal(obj)
   334  		if err != nil {
   335  			return b, err
   336  		}
   337  		b.req.Body = ioutil.NopCloser(bytes.NewReader(byts))
   338  		b.req.ContentLength = int64(len(byts))
   339  		b.req.Header.Set("Content-Type", "application/xml")
   340  	}
   341  	return b, nil
   342  }
   343  
   344  // YAMLBody adds request raw body encoding by YAML.
   345  func (b *BeegoHTTPRequest) YAMLBody(obj interface{}) (*BeegoHTTPRequest, error) {
   346  	if b.req.Body == nil && obj != nil {
   347  		byts, err := yaml.Marshal(obj)
   348  		if err != nil {
   349  			return b, err
   350  		}
   351  		b.req.Body = ioutil.NopCloser(bytes.NewReader(byts))
   352  		b.req.ContentLength = int64(len(byts))
   353  		b.req.Header.Set("Content-Type", "application/x+yaml")
   354  	}
   355  	return b, nil
   356  }
   357  
   358  // JSONBody adds request raw body encoding by JSON.
   359  func (b *BeegoHTTPRequest) JSONBody(obj interface{}) (*BeegoHTTPRequest, error) {
   360  	if b.req.Body == nil && obj != nil {
   361  		byts, err := json.Marshal(obj)
   362  		if err != nil {
   363  			return b, err
   364  		}
   365  		b.req.Body = ioutil.NopCloser(bytes.NewReader(byts))
   366  		b.req.ContentLength = int64(len(byts))
   367  		b.req.Header.Set("Content-Type", "application/json")
   368  	}
   369  	return b, nil
   370  }
   371  
   372  func (b *BeegoHTTPRequest) buildURL(paramBody string) {
   373  	// build GET url with query string
   374  	if b.req.Method == "GET" && len(paramBody) > 0 {
   375  		if strings.Contains(b.url, "?") {
   376  			b.url += "&" + paramBody
   377  		} else {
   378  			b.url = b.url + "?" + paramBody
   379  		}
   380  		return
   381  	}
   382  
   383  	// build POST/PUT/PATCH url and body
   384  	if (b.req.Method == "POST" || b.req.Method == "PUT" || b.req.Method == "PATCH" || b.req.Method == "DELETE") && b.req.Body == nil {
   385  		// with files
   386  		if len(b.files) > 0 {
   387  			pr, pw := io.Pipe()
   388  			bodyWriter := multipart.NewWriter(pw)
   389  			go func() {
   390  				for formname, filename := range b.files {
   391  					fileWriter, err := bodyWriter.CreateFormFile(formname, filename)
   392  					if err != nil {
   393  						log.Println("Httplib:", err)
   394  					}
   395  					fh, err := os.Open(filename)
   396  					if err != nil {
   397  						log.Println("Httplib:", err)
   398  					}
   399  					//iocopy
   400  					_, err = io.Copy(fileWriter, fh)
   401  					fh.Close()
   402  					if err != nil {
   403  						log.Println("Httplib:", err)
   404  					}
   405  				}
   406  				for k, v := range b.params {
   407  					for _, vv := range v {
   408  						bodyWriter.WriteField(k, vv)
   409  					}
   410  				}
   411  				bodyWriter.Close()
   412  				pw.Close()
   413  			}()
   414  			b.Header("Content-Type", bodyWriter.FormDataContentType())
   415  			b.req.Body = ioutil.NopCloser(pr)
   416  			b.Header("Transfer-Encoding", "chunked")
   417  			return
   418  		}
   419  
   420  		// with params
   421  		if len(paramBody) > 0 {
   422  			b.Header("Content-Type", "application/x-www-form-urlencoded")
   423  			b.Body(paramBody)
   424  		}
   425  	}
   426  }
   427  
   428  func (b *BeegoHTTPRequest) getResponse() (*http.Response, error) {
   429  	if b.resp.StatusCode != 0 {
   430  		return b.resp, nil
   431  	}
   432  	resp, err := b.DoRequest()
   433  	if err != nil {
   434  		return nil, err
   435  	}
   436  	b.resp = resp
   437  	return resp, nil
   438  }
   439  
   440  // DoRequest will do the client.Do
   441  func (b *BeegoHTTPRequest) DoRequest() (resp *http.Response, err error) {
   442  	var paramBody string
   443  	if len(b.params) > 0 {
   444  		var buf bytes.Buffer
   445  		for k, v := range b.params {
   446  			for _, vv := range v {
   447  				buf.WriteString(url.QueryEscape(k))
   448  				buf.WriteByte('=')
   449  				buf.WriteString(url.QueryEscape(vv))
   450  				buf.WriteByte('&')
   451  			}
   452  		}
   453  		paramBody = buf.String()
   454  		paramBody = paramBody[0 : len(paramBody)-1]
   455  	}
   456  
   457  	b.buildURL(paramBody)
   458  	urlParsed, err := url.Parse(b.url)
   459  	if err != nil {
   460  		return nil, err
   461  	}
   462  
   463  	b.req.URL = urlParsed
   464  
   465  	trans := b.setting.Transport
   466  
   467  	if trans == nil {
   468  		// create default transport
   469  		trans = &http.Transport{
   470  			TLSClientConfig:     b.setting.TLSClientConfig,
   471  			Proxy:               b.setting.Proxy,
   472  			Dial:                TimeoutDialer(b.setting.ConnectTimeout, b.setting.ReadWriteTimeout),
   473  			MaxIdleConnsPerHost: 100,
   474  		}
   475  	} else {
   476  		// if b.transport is *http.Transport then set the settings.
   477  		if t, ok := trans.(*http.Transport); ok {
   478  			if t.TLSClientConfig == nil {
   479  				t.TLSClientConfig = b.setting.TLSClientConfig
   480  			}
   481  			if t.Proxy == nil {
   482  				t.Proxy = b.setting.Proxy
   483  			}
   484  			if t.Dial == nil {
   485  				t.Dial = TimeoutDialer(b.setting.ConnectTimeout, b.setting.ReadWriteTimeout)
   486  			}
   487  		}
   488  	}
   489  
   490  	var jar http.CookieJar
   491  	if b.setting.EnableCookie {
   492  		if defaultCookieJar == nil {
   493  			createDefaultCookie()
   494  		}
   495  		jar = defaultCookieJar
   496  	}
   497  
   498  	client := &http.Client{
   499  		Transport: trans,
   500  		Jar:       jar,
   501  	}
   502  
   503  	if b.setting.UserAgent != "" && b.req.Header.Get("User-Agent") == "" {
   504  		b.req.Header.Set("User-Agent", b.setting.UserAgent)
   505  	}
   506  
   507  	if b.setting.CheckRedirect != nil {
   508  		client.CheckRedirect = b.setting.CheckRedirect
   509  	}
   510  
   511  	if b.setting.ShowDebug {
   512  		dump, err := httputil.DumpRequest(b.req, b.setting.DumpBody)
   513  		if err != nil {
   514  			log.Println(err.Error())
   515  		}
   516  		b.dump = dump
   517  	}
   518  	// retries default value is 0, it will run once.
   519  	// retries equal to -1, it will run forever until success
   520  	// retries is setted, it will retries fixed times.
   521  	// Sleeps for a 400ms in between calls to reduce spam
   522  	for i := 0; b.setting.Retries == -1 || i <= b.setting.Retries; i++ {
   523  		resp, err = client.Do(b.req)
   524  		if err == nil {
   525  			break
   526  		}
   527  		time.Sleep(b.setting.RetryDelay)
   528  	}
   529  	return resp, err
   530  }
   531  
   532  // String returns the body string in response.
   533  // it calls Response inner.
   534  func (b *BeegoHTTPRequest) String() (string, error) {
   535  	data, err := b.Bytes()
   536  	if err != nil {
   537  		return "", err
   538  	}
   539  
   540  	return string(data), nil
   541  }
   542  
   543  // Bytes returns the body []byte in response.
   544  // it calls Response inner.
   545  func (b *BeegoHTTPRequest) Bytes() ([]byte, error) {
   546  	if b.body != nil {
   547  		return b.body, nil
   548  	}
   549  	resp, err := b.getResponse()
   550  	if err != nil {
   551  		return nil, err
   552  	}
   553  	if resp.Body == nil {
   554  		return nil, nil
   555  	}
   556  	defer resp.Body.Close()
   557  	if b.setting.Gzip && resp.Header.Get("Content-Encoding") == "gzip" {
   558  		reader, err := gzip.NewReader(resp.Body)
   559  		if err != nil {
   560  			return nil, err
   561  		}
   562  		b.body, err = ioutil.ReadAll(reader)
   563  		return b.body, err
   564  	}
   565  	b.body, err = ioutil.ReadAll(resp.Body)
   566  	return b.body, err
   567  }
   568  
   569  // ToFile saves the body data in response to one file.
   570  // it calls Response inner.
   571  func (b *BeegoHTTPRequest) ToFile(filename string) error {
   572  	resp, err := b.getResponse()
   573  	if err != nil {
   574  		return err
   575  	}
   576  	if resp.Body == nil {
   577  		return nil
   578  	}
   579  	defer resp.Body.Close()
   580  	err = pathExistAndMkdir(filename)
   581  	if err != nil {
   582  		return err
   583  	}
   584  	f, err := os.Create(filename)
   585  	if err != nil {
   586  		return err
   587  	}
   588  	defer f.Close()
   589  	_, err = io.Copy(f, resp.Body)
   590  	return err
   591  }
   592  
   593  //Check that the file directory exists, there is no automatically created
   594  func pathExistAndMkdir(filename string) (err error) {
   595  	filename = path.Dir(filename)
   596  	_, err = os.Stat(filename)
   597  	if err == nil {
   598  		return nil
   599  	}
   600  	if os.IsNotExist(err) {
   601  		err = os.MkdirAll(filename, os.ModePerm)
   602  		if err == nil {
   603  			return nil
   604  		}
   605  	}
   606  	return err
   607  }
   608  
   609  // ToJSON returns the map that marshals from the body bytes as json in response .
   610  // it calls Response inner.
   611  func (b *BeegoHTTPRequest) ToJSON(v interface{}) error {
   612  	data, err := b.Bytes()
   613  	if err != nil {
   614  		return err
   615  	}
   616  	return json.Unmarshal(data, v)
   617  }
   618  
   619  // ToXML returns the map that marshals from the body bytes as xml in response .
   620  // it calls Response inner.
   621  func (b *BeegoHTTPRequest) ToXML(v interface{}) error {
   622  	data, err := b.Bytes()
   623  	if err != nil {
   624  		return err
   625  	}
   626  	return xml.Unmarshal(data, v)
   627  }
   628  
   629  // ToYAML returns the map that marshals from the body bytes as yaml in response .
   630  // it calls Response inner.
   631  func (b *BeegoHTTPRequest) ToYAML(v interface{}) error {
   632  	data, err := b.Bytes()
   633  	if err != nil {
   634  		return err
   635  	}
   636  	return yaml.Unmarshal(data, v)
   637  }
   638  
   639  // Response executes request client gets response mannually.
   640  func (b *BeegoHTTPRequest) Response() (*http.Response, error) {
   641  	return b.getResponse()
   642  }
   643  
   644  // TimeoutDialer returns functions of connection dialer with timeout settings for http.Transport Dial field.
   645  func TimeoutDialer(cTimeout time.Duration, rwTimeout time.Duration) func(net, addr string) (c net.Conn, err error) {
   646  	return func(netw, addr string) (net.Conn, error) {
   647  		conn, err := net.DialTimeout(netw, addr, cTimeout)
   648  		if err != nil {
   649  			return nil, err
   650  		}
   651  		err = conn.SetDeadline(time.Now().Add(rwTimeout))
   652  		return conn, err
   653  	}
   654  }