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 }