github.com/GuanceCloud/cliutils@v1.1.21/dialtesting/http.go (about)

     1  // Unless explicitly stated otherwise all files in this repository are licensed
     2  // under the MIT License.
     3  // This product includes software developed at Guance Cloud (https://www.guance.com/).
     4  // Copyright 2021-present Guance, Inc.
     5  
     6  package dialtesting
     7  
     8  // HTTP dialer testing
     9  // auth: tanb
    10  // date: Fri Feb  5 13:17:00 CST 2021
    11  
    12  import (
    13  	"crypto/tls"
    14  	"crypto/x509"
    15  	"encoding/json"
    16  	"fmt"
    17  	"io"
    18  	"net"
    19  	"net/http"
    20  	"net/http/httptrace"
    21  	"net/url"
    22  	"strings"
    23  	"time"
    24  
    25  	"github.com/GuanceCloud/cliutils"
    26  )
    27  
    28  type HTTPTask struct {
    29  	ExternalID        string             `json:"external_id"`
    30  	Name              string             `json:"name"`
    31  	AK                string             `json:"access_key"`
    32  	Method            string             `json:"method"`
    33  	URL               string             `json:"url"`
    34  	PostURL           string             `json:"post_url"`
    35  	CurStatus         string             `json:"status"`
    36  	Frequency         string             `json:"frequency"`
    37  	Region            string             `json:"region"` // 冗余进来,便于调试
    38  	OwnerExternalID   string             `json:"owner_external_id"`
    39  	SuccessWhenLogic  string             `json:"success_when_logic"`
    40  	SuccessWhen       []*HTTPSuccess     `json:"success_when"`
    41  	Tags              map[string]string  `json:"tags,omitempty"`
    42  	Labels            []string           `json:"labels,omitempty"`
    43  	WorkspaceLanguage string             `json:"workspace_language,omitempty"`
    44  	TagsInfo          string             `json:"tags_info,omitempty"`
    45  	AdvanceOptions    *HTTPAdvanceOption `json:"advance_options,omitempty"`
    46  	UpdateTime        int64              `json:"update_time,omitempty"`
    47  	Option            map[string]string
    48  
    49  	ticker   *time.Ticker
    50  	cli      *http.Client
    51  	resp     *http.Response
    52  	req      *http.Request
    53  	respBody []byte
    54  	reqStart time.Time
    55  	reqCost  time.Duration
    56  	reqError string
    57  
    58  	dnsParseTime   float64
    59  	connectionTime float64
    60  	sslTime        float64
    61  	ttfbTime       float64
    62  	downloadTime   float64
    63  
    64  	destIP string
    65  }
    66  
    67  const MaxMsgSize = 15 * 1024 * 1024
    68  
    69  func (t *HTTPTask) UpdateTimeUs() int64 {
    70  	return t.UpdateTime
    71  }
    72  
    73  func (t *HTTPTask) Clear() {
    74  	t.dnsParseTime = 0.0
    75  	t.connectionTime = 0.0
    76  	t.sslTime = 0.0
    77  	t.downloadTime = 0.0
    78  	t.ttfbTime = 0.0
    79  	t.reqCost = 0
    80  
    81  	t.resp = nil
    82  	t.respBody = []byte(``)
    83  	t.reqError = ""
    84  }
    85  
    86  func (t *HTTPTask) ID() string {
    87  	if t.ExternalID == `` {
    88  		return cliutils.XID("dtst_")
    89  	}
    90  	return fmt.Sprintf("%s_%s", t.AK, t.ExternalID)
    91  }
    92  
    93  func (t *HTTPTask) GetOwnerExternalID() string {
    94  	return t.OwnerExternalID
    95  }
    96  
    97  func (t *HTTPTask) SetOwnerExternalID(exid string) {
    98  	t.OwnerExternalID = exid
    99  }
   100  
   101  func (t *HTTPTask) SetRegionID(regionID string) {
   102  	t.Region = regionID
   103  }
   104  
   105  func (t *HTTPTask) SetAk(ak string) {
   106  	t.AK = ak
   107  }
   108  
   109  func (t *HTTPTask) SetStatus(status string) {
   110  	t.CurStatus = status
   111  }
   112  
   113  func (t *HTTPTask) SetUpdateTime(ts int64) {
   114  	t.UpdateTime = ts
   115  }
   116  
   117  func (t *HTTPTask) Stop() error {
   118  	if t.cli != nil {
   119  		t.cli.CloseIdleConnections()
   120  	}
   121  	return nil
   122  }
   123  
   124  func (t *HTTPTask) Status() string {
   125  	return t.CurStatus
   126  }
   127  
   128  func (t *HTTPTask) Ticker() *time.Ticker {
   129  	return t.ticker
   130  }
   131  
   132  func (t *HTTPTask) Class() string {
   133  	return "HTTP"
   134  }
   135  
   136  func (t *HTTPTask) MetricName() string {
   137  	return `http_dial_testing`
   138  }
   139  
   140  func (t *HTTPTask) PostURLStr() string {
   141  	return t.PostURL
   142  }
   143  
   144  func (t *HTTPTask) GetFrequency() string {
   145  	return t.Frequency
   146  }
   147  
   148  func (t *HTTPTask) GetLineData() string {
   149  	return ""
   150  }
   151  
   152  func (t *HTTPTask) GetResults() (tags map[string]string, fields map[string]interface{}) {
   153  	tags = map[string]string{
   154  		"name":    t.Name,
   155  		"url":     t.URL,
   156  		"proto":   t.req.Proto,
   157  		"status":  "FAIL",
   158  		"method":  t.Method,
   159  		"dest_ip": t.destIP,
   160  	}
   161  
   162  	fields = map[string]interface{}{
   163  		"response_time":      int64(t.reqCost) / 1000, // 单位为us
   164  		"response_body_size": int64(len(t.respBody)),
   165  		"success":            int64(-1),
   166  	}
   167  
   168  	if t.resp != nil {
   169  		fields["status_code"] = t.resp.StatusCode
   170  		tags["status_code_string"] = t.resp.Status
   171  		tags["status_code_class"] = fmt.Sprintf(`%dxx`, t.resp.StatusCode/100)
   172  	}
   173  
   174  	for k, v := range t.Tags {
   175  		tags[k] = v
   176  	}
   177  
   178  	message := map[string]interface{}{}
   179  
   180  	if t.req != nil {
   181  		message[`request_body`] = t.req.Body
   182  		message[`request_header`] = t.req.Header
   183  	}
   184  
   185  	reasons, succFlag := t.CheckResult()
   186  	if t.reqError != "" {
   187  		reasons = append(reasons, t.reqError)
   188  	}
   189  	switch t.SuccessWhenLogic {
   190  	case "or":
   191  		if succFlag && t.reqError == "" {
   192  			tags["status"] = "OK"
   193  			fields["success"] = int64(1)
   194  		} else {
   195  			message[`fail_reason`] = strings.Join(reasons, `;`)
   196  			fields[`fail_reason`] = strings.Join(reasons, `;`)
   197  		}
   198  	default:
   199  		if len(reasons) != 0 {
   200  			message[`fail_reason`] = strings.Join(reasons, `;`)
   201  			fields[`fail_reason`] = strings.Join(reasons, `;`)
   202  		}
   203  
   204  		if t.reqError == "" && len(reasons) == 0 {
   205  			tags["status"] = "OK"
   206  			fields["success"] = int64(1)
   207  		}
   208  	}
   209  
   210  	notSave := false
   211  	if t.AdvanceOptions != nil && t.AdvanceOptions.Secret != nil && t.AdvanceOptions.Secret.NoSaveResponseBody {
   212  		notSave = true
   213  	}
   214  
   215  	if v, ok := fields[`fail_reason`]; ok && !notSave && len(v.(string)) != 0 && t.resp != nil {
   216  		message[`response_header`] = t.resp.Header
   217  		message[`response_body`] = string(t.respBody)
   218  	}
   219  
   220  	fields[`response_dns`] = t.dnsParseTime
   221  	fields[`response_connection`] = t.connectionTime
   222  	fields[`response_ssl`] = t.sslTime
   223  	fields[`response_ttfb`] = t.ttfbTime
   224  	fields[`response_download`] = t.downloadTime
   225  
   226  	data, err := json.Marshal(message)
   227  	if err != nil {
   228  		fields[`message`] = err.Error()
   229  	}
   230  
   231  	if len(data) > MaxMsgSize {
   232  		fields[`message`] = string(data[:MaxMsgSize])
   233  	} else {
   234  		fields[`message`] = string(data)
   235  	}
   236  
   237  	return tags, fields
   238  }
   239  
   240  func (t *HTTPTask) RegionName() string {
   241  	return t.Region
   242  }
   243  
   244  func (t *HTTPTask) AccessKey() string {
   245  	return t.AK
   246  }
   247  
   248  func (t *HTTPTask) Check() error {
   249  	// TODO: check task validity
   250  	if t.ExternalID == "" {
   251  		return fmt.Errorf("external ID missing")
   252  	}
   253  
   254  	return t.Init()
   255  }
   256  
   257  type HTTPSuccess struct {
   258  	Body []*SuccessOption `json:"body,omitempty"`
   259  
   260  	ResponseTime string `json:"response_time,omitempty"`
   261  	respTime     time.Duration
   262  
   263  	Header     map[string][]*SuccessOption `json:"header,omitempty"`
   264  	StatusCode []*SuccessOption            `json:"status_code,omitempty"`
   265  }
   266  
   267  type HTTPOptAuth struct {
   268  	// basic auth
   269  	Username string `json:"username,omitempty"`
   270  	Password string `json:"password,omitempty"`
   271  	// TODO: 支持更多的 auth 选项
   272  }
   273  
   274  type HTTPOptRequest struct {
   275  	FollowRedirect bool              `json:"follow_redirect,omitempty"`
   276  	Headers        map[string]string `json:"headers,omitempty"`
   277  	Cookies        string            `json:"cookies,omitempty"`
   278  	Auth           *HTTPOptAuth      `json:"auth,omitempty"`
   279  }
   280  
   281  type HTTPOptBody struct {
   282  	BodyType string `json:"body_type,omitempty"`
   283  	Body     string `json:"body,omitempty"`
   284  }
   285  
   286  type HTTPOptCertificate struct {
   287  	IgnoreServerCertificateError bool   `json:"ignore_server_certificate_error,omitempty"`
   288  	PrivateKey                   string `json:"private_key,omitempty"`
   289  	Certificate                  string `json:"certificate,omitempty"`
   290  	CaCert                       string `json:"ca,omitempty"`
   291  }
   292  
   293  type HTTPOptProxy struct {
   294  	URL     string            `json:"url,omitempty"`
   295  	Headers map[string]string `json:"headers,omitempty"`
   296  }
   297  
   298  type HTTPAdvanceOption struct {
   299  	RequestOptions *HTTPOptRequest     `json:"request_options,omitempty"`
   300  	RequestBody    *HTTPOptBody        `json:"request_body,omitempty"`
   301  	Certificate    *HTTPOptCertificate `json:"certificate,omitempty"`
   302  	Proxy          *HTTPOptProxy       `json:"proxy,omitempty"`
   303  	Secret         *HTTPSecret         `json:"secret,omitempty"`
   304  	RequestTimeout string              `json:"request_timeout,omitempty"`
   305  }
   306  
   307  type HTTPSecret struct {
   308  	NoSaveResponseBody bool `json:"not_save,omitempty"`
   309  }
   310  
   311  func (t *HTTPTask) Run() error {
   312  	t.Clear()
   313  
   314  	var t1, connect, dns, tlsHandshake time.Time
   315  	var body io.Reader = nil
   316  
   317  	trace := &httptrace.ClientTrace{
   318  		DNSStart: func(dsi httptrace.DNSStartInfo) { dns = time.Now() },
   319  		DNSDone: func(ddi httptrace.DNSDoneInfo) {
   320  			t.dnsParseTime = float64(time.Since(dns)) / float64(time.Microsecond)
   321  		},
   322  
   323  		TLSHandshakeStart: func() { tlsHandshake = time.Now() },
   324  		TLSHandshakeDone: func(cs tls.ConnectionState, err error) {
   325  			t.sslTime = float64(time.Since(tlsHandshake)) / float64(time.Microsecond)
   326  		},
   327  
   328  		ConnectStart: func(network, addr string) { connect = time.Now() },
   329  		ConnectDone: func(network, addr string, err error) {
   330  			t.connectionTime = float64(time.Since(connect)) / float64(time.Microsecond)
   331  			if host, _, err := net.SplitHostPort(addr); err == nil {
   332  				t.destIP = host
   333  			} else {
   334  				t.destIP = addr
   335  			}
   336  		},
   337  
   338  		GotFirstResponseByte: func() {
   339  			t1 = time.Now()
   340  			t.ttfbTime = float64(time.Since(t.reqStart)) / float64(time.Microsecond)
   341  		},
   342  	}
   343  
   344  	reqURL, err := url.Parse(t.URL)
   345  	if err != nil {
   346  		goto result
   347  	}
   348  
   349  	if t.AdvanceOptions != nil && t.AdvanceOptions.RequestBody != nil && t.AdvanceOptions.RequestBody.Body != "" {
   350  		body = strings.NewReader(t.AdvanceOptions.RequestBody.Body)
   351  	}
   352  
   353  	t.req, err = http.NewRequest(t.Method, reqURL.String(), body)
   354  	if err != nil {
   355  		goto result
   356  	}
   357  
   358  	// advance options
   359  	if err := t.setupAdvanceOpts(t.req); err != nil {
   360  		goto result
   361  	}
   362  
   363  	t.req = t.req.WithContext(httptrace.WithClientTrace(t.req.Context(), trace))
   364  
   365  	t.req.Header.Add("Connection", "close")
   366  
   367  	if agentInfo, ok := t.Option["userAgent"]; ok {
   368  		t.req.Header.Add("User-Agent", agentInfo)
   369  	}
   370  
   371  	t.reqStart = time.Now()
   372  	t.resp, err = t.cli.Do(t.req)
   373  	if t.resp != nil {
   374  		defer t.resp.Body.Close() //nolint:errcheck
   375  	}
   376  
   377  	if err != nil {
   378  		goto result
   379  	}
   380  
   381  	t.respBody, err = io.ReadAll(t.resp.Body)
   382  	t.reqCost = time.Since(t.reqStart)
   383  	if err != nil {
   384  		goto result
   385  	}
   386  
   387  	t.downloadTime = float64(time.Since(t1)) / float64(time.Microsecond)
   388  
   389  result:
   390  	if err != nil {
   391  		t.reqError = err.Error()
   392  	}
   393  
   394  	return err
   395  }
   396  
   397  func (t *HTTPTask) CheckResult() (reasons []string, succFlag bool) {
   398  	if t.resp == nil {
   399  		return nil, true
   400  	}
   401  
   402  	for _, chk := range t.SuccessWhen {
   403  		// check headers
   404  
   405  		for k, vs := range chk.Header {
   406  			for _, v := range vs {
   407  				if err := v.check(t.resp.Header.Get(k), fmt.Sprintf("HTTP header `%s'", k)); err != nil {
   408  					reasons = append(reasons, err.Error())
   409  				} else {
   410  					succFlag = true
   411  				}
   412  			}
   413  		}
   414  
   415  		// check body
   416  		if chk.Body != nil {
   417  			for _, v := range chk.Body {
   418  				if err := v.check(string(t.respBody), "response body"); err != nil {
   419  					reasons = append(reasons, err.Error())
   420  				} else {
   421  					succFlag = true
   422  				}
   423  			}
   424  		}
   425  
   426  		// check status code
   427  		if chk.StatusCode != nil {
   428  			for _, v := range chk.StatusCode {
   429  				if err := v.check(fmt.Sprintf(`%d`, t.resp.StatusCode), "HTTP status"); err != nil {
   430  					reasons = append(reasons, err.Error())
   431  				} else {
   432  					succFlag = true
   433  				}
   434  			}
   435  		}
   436  
   437  		// check response time
   438  		if t.reqCost > chk.respTime && chk.respTime > 0 {
   439  			reasons = append(reasons,
   440  				fmt.Sprintf("HTTP response time(%v) larger than %v", t.reqCost, chk.respTime))
   441  		} else if chk.respTime > 0 {
   442  			succFlag = true
   443  		}
   444  	}
   445  
   446  	return reasons, succFlag
   447  }
   448  
   449  func (t *HTTPTask) setupAdvanceOpts(req *http.Request) error {
   450  	opt := t.AdvanceOptions
   451  
   452  	if opt == nil {
   453  		return nil
   454  	}
   455  
   456  	// request options
   457  	if opt.RequestOptions != nil {
   458  		// headers
   459  		for k, v := range opt.RequestOptions.Headers {
   460  			if k == "Host" || k == "host" {
   461  				req.Host = v
   462  			} else {
   463  				req.Header.Add(k, v)
   464  			}
   465  		}
   466  
   467  		// cookie
   468  		if opt.RequestOptions.Cookies != "" {
   469  			req.Header.Add("Cookie", opt.RequestOptions.Cookies)
   470  		}
   471  
   472  		// auth
   473  		// TODO: add more auth options
   474  		if opt.RequestOptions.Auth != nil {
   475  			if !(opt.RequestOptions.Auth.Username == "" && opt.RequestOptions.Auth.Password == "") {
   476  				req.SetBasicAuth(opt.RequestOptions.Auth.Username, opt.RequestOptions.Auth.Password)
   477  			}
   478  		}
   479  	}
   480  
   481  	// body options
   482  	if opt.RequestBody != nil {
   483  		if opt.RequestBody.BodyType != "" {
   484  			req.Header.Add("Content-Type", opt.RequestBody.BodyType)
   485  		}
   486  	}
   487  
   488  	// proxy headers
   489  	if opt.Proxy != nil { // see https://stackoverflow.com/a/14663620/342348
   490  		for k, v := range opt.Proxy.Headers {
   491  			req.Header.Add(k, v)
   492  		}
   493  	}
   494  
   495  	return nil
   496  }
   497  
   498  func (t *HTTPTask) InitDebug() error {
   499  	return t.init(true)
   500  }
   501  
   502  func (t *HTTPTask) Init() error {
   503  	return t.init(false)
   504  }
   505  
   506  func (t *HTTPTask) init(debug bool) error {
   507  	httpTimeout := 30 * time.Second // default timeout
   508  	if !debug {
   509  		// setup frequency
   510  		du, err := time.ParseDuration(t.Frequency)
   511  		if err != nil {
   512  			return err
   513  		}
   514  		if t.ticker != nil {
   515  			t.ticker.Stop()
   516  		}
   517  		t.ticker = time.NewTicker(du)
   518  	}
   519  
   520  	if t.Option == nil {
   521  		t.Option = map[string]string{}
   522  	}
   523  
   524  	if strings.EqualFold(t.CurStatus, StatusStop) {
   525  		return nil
   526  	}
   527  
   528  	// advance options
   529  	opt := t.AdvanceOptions
   530  
   531  	if opt != nil && opt.RequestTimeout != "" {
   532  		du, err := time.ParseDuration(opt.RequestTimeout)
   533  		if err != nil {
   534  			return err
   535  		}
   536  
   537  		httpTimeout = du
   538  	}
   539  
   540  	// setup HTTP client
   541  	t.cli = &http.Client{
   542  		Timeout: httpTimeout,
   543  	}
   544  
   545  	if opt != nil {
   546  		if opt.RequestOptions != nil {
   547  			// check FollowRedirect
   548  			if !opt.RequestOptions.FollowRedirect { // see https://stackoverflow.com/a/38150816/342348
   549  				t.cli.CheckRedirect = func(req *http.Request, via []*http.Request) error {
   550  					return http.ErrUseLastResponse
   551  				}
   552  			}
   553  		}
   554  
   555  		if opt.RequestBody != nil {
   556  			switch opt.RequestBody.BodyType {
   557  			case "text/plain", "application/json", "text/xml", "application/x-www-form-urlencoded":
   558  			case "text/html", "multipart/form-data", "", "None": // do nothing
   559  			default:
   560  				return fmt.Errorf("invalid body type: `%s'", opt.RequestBody.BodyType)
   561  			}
   562  		}
   563  
   564  		// TLS opotions
   565  		if opt.Certificate != nil { // see https://venilnoronha.io/a-step-by-step-guide-to-mtls-in-go
   566  			if opt.Certificate.IgnoreServerCertificateError {
   567  				t.cli.Transport = &http.Transport{
   568  					TLSClientConfig: &tls.Config{
   569  						InsecureSkipVerify: opt.Certificate.IgnoreServerCertificateError, //nolint:gosec
   570  					},
   571  				}
   572  			} else if opt.Certificate.CaCert != "" {
   573  				caCertPool := x509.NewCertPool()
   574  				caCertPool.AppendCertsFromPEM([]byte(opt.Certificate.CaCert))
   575  
   576  				cert, err := tls.X509KeyPair([]byte(opt.Certificate.Certificate), []byte(opt.Certificate.PrivateKey))
   577  				if err != nil {
   578  					return err
   579  				}
   580  
   581  				t.cli.Transport = &http.Transport{
   582  					TLSClientConfig: &tls.Config{ //nolint:gosec
   583  						RootCAs:      caCertPool,
   584  						Certificates: []tls.Certificate{cert},
   585  					},
   586  				}
   587  			}
   588  		}
   589  
   590  		// proxy options
   591  		if opt.Proxy != nil { // see https://stackoverflow.com/a/14663620/342348
   592  			proxyURL, err := url.Parse(opt.Proxy.URL)
   593  			if err != nil {
   594  				return err
   595  			}
   596  
   597  			if t.cli.Transport == nil {
   598  				t.cli.Transport = &http.Transport{Proxy: http.ProxyURL(proxyURL)}
   599  			} else {
   600  				t.cli.Transport.(*http.Transport).Proxy = http.ProxyURL(proxyURL)
   601  			}
   602  		}
   603  	}
   604  
   605  	if len(t.SuccessWhen) == 0 {
   606  		return fmt.Errorf(`no any check rule`)
   607  	}
   608  
   609  	// init success checker
   610  	for _, checker := range t.SuccessWhen {
   611  		if checker.ResponseTime != "" {
   612  			du, err := time.ParseDuration(checker.ResponseTime)
   613  			if err != nil {
   614  				return err
   615  			}
   616  			checker.respTime = du
   617  		}
   618  
   619  		for _, vs := range checker.Header {
   620  			for _, v := range vs {
   621  				err := genReg(v)
   622  				if err != nil {
   623  					return err
   624  				}
   625  			}
   626  		}
   627  
   628  		// body
   629  		for _, v := range checker.Body {
   630  			err := genReg(v)
   631  			if err != nil {
   632  				return err
   633  			}
   634  		}
   635  
   636  		// status_code
   637  		for _, v := range checker.StatusCode {
   638  			err := genReg(v)
   639  			if err != nil {
   640  				return err
   641  			}
   642  		}
   643  	}
   644  
   645  	// TODO: more checking on task validity
   646  
   647  	return nil
   648  }
   649  
   650  func (t *HTTPTask) GetHostName() (string, error) {
   651  	return getHostName(t.URL)
   652  }
   653  
   654  func (t *HTTPTask) GetWorkspaceLanguage() string {
   655  	if t.WorkspaceLanguage == "en" {
   656  		return "en"
   657  	}
   658  	return "zh"
   659  }
   660  
   661  func (t *HTTPTask) GetTagsInfo() string {
   662  	return t.TagsInfo
   663  }