// Package chttp provides HTTP check functionality for RSMon. package chttp import ( "bytes" "encoding/json" "io" "mime/multipart" "net" "net/http" "strings" "time" "github.com/pkg/errors" "rocketgit.ru/rsmon/worker/app/models" ) const ( stateERR = "ERR" stateFAIL = "FAIL" ) // UserAgent is the HTTP User-Agent header sent with all HTTP checks. const UserAgent = "Mozilla/5.0 (compatible; RSMon/1.0; +https://rsmon.ru/bot)" func ipv6(ip []string) []string { var ips []string for _, a := range ip { ip := net.ParseIP(a) check := ip.To4() if check == nil { ips = append(ips, a) } } return ips } // Perform executes an HTTP check. func Perform(c *models.Check) *Result { result := &Result{} result.State = "START" if c.URL == nil { result.State = stateFAIL result.Error = errors.New("no url or bad url") return result } var requestBody io.Reader settings := c.GetSettings() var err error var to time.Duration var slow time.Duration if settings.Timeout > 0 && settings.Timeout < 300000 { to = time.Millisecond * time.Duration(settings.Timeout) } else { to = time.Second * 60 } if settings.SlowTime > 0 { slow = time.Millisecond * time.Duration(settings.SlowTime) } else { slow = time.Second * 5 } client := &http.Client{ Timeout: to, CheckRedirect: func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse }, } if settings.CheckIp { addr, err := net.LookupHost(*c.URL) if err != nil { result.Status = stateFAIL result.Error = err } if settings.CheckIPv6 { addr = ipv6(addr) } for _, a := range addr { _, err := net.Dial("tcp", net.JoinHostPort(a, "80")) if err != nil { result.Error = err break } result.State = "OK" } } switch settings.RequestType { case "application/json": body, _ := json.Marshal(settings.RequestContent) requestBody = bytes.NewReader(body) case "application/x-www-form-urlencoded": requestBody = strings.NewReader(settings.RequestContent) case "multipart/form-data": buf := bytes.NewBuffer([]byte{}) writer := multipart.NewWriter(buf) _, _ = writer.CreateFormField(settings.RequestContent) _ = writer.Close() requestBody = bytes.NewReader(buf.Bytes()) settings.RequestType = writer.FormDataContentType() case "text/plain": requestBody = bytes.NewBufferString(settings.RequestContent) } if settings.RequestMethod == "" { settings.RequestMethod = "GET" } request, err := http.NewRequest(strings.ToUpper(settings.RequestMethod), *c.URL, requestBody) if err != nil { result.State = stateFAIL result.Error = err return result } if settings.HTTPUsername != "" && settings.HTTPPassword != "" { request.SetBasicAuth(settings.HTTPUsername, settings.HTTPPassword) } request.Header.Set("User-Agent", UserAgent) request.Header.Set("Cache-Control", "max-age=0") request.Header.Set("Connection", "close") if settings.RequestType != "" { request.Header.Set("Content-Type", settings.RequestType) } if len(settings.RequestHeader) != 0 { for _, h := range settings.RequestHeader { request.Header.Set(h.Key, h.Value) } } start := time.Now() resp, err := client.Do(request) if err != nil { result.State = stateERR result.Error = errors.Wrap(err, "request exec") return result } defer resp.Body.Close() //nolint:errcheck // accepted lint exception result.Status = resp.Status result.StatusCode = resp.StatusCode result.Headers = resp.Header body, err := io.ReadAll(resp.Body) result.Duration = time.Since(start) if err != nil { result.State = stateERR result.Error = errors.Wrap(err, "read body") return result } result.Length = len(body) result.Body = body warns, err := settings.CheckAnswer(resp, body) for _, w := range warns { result.State = "WARN" result.Warnings = append(result.Warnings, w) } if err != nil { result.State = stateERR result.Error = errors.Wrap(err, "response check") } if result.Error == nil { if result.Duration > slow { result.State = "WARN" result.Warnings = append(result.Warnings, "slow") } else if result.State == "START" { result.State = "OK" } } else { result.State = stateERR } return result }