Skip to content
This repository has been archived by the owner on Mar 27, 2024. It is now read-only.

Commit

Permalink
add cookie_file config option (#1133)
Browse files Browse the repository at this point in the history
  • Loading branch information
ilyam8 committed Mar 21, 2023
1 parent ea3c998 commit 0f6efe0
Show file tree
Hide file tree
Showing 8 changed files with 443 additions and 285 deletions.
4 changes: 0 additions & 4 deletions modules/httpcheck/charts.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,6 @@ var responseStatusChart = module.Chart{
{ID: "timeout"},
{ID: "bad_content"},
{ID: "bad_status"},
//{ID: "dns_lookup_error", Name: "dns lookup error"},
//{ID: "address_parse_error", Name: "address parse error"},
//{ID: "redirect_error", Name: "redirect error"},
//{ID: "body_read_error", Name: "body read error"},
},
}

Expand Down
88 changes: 41 additions & 47 deletions modules/httpcheck/collect.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"io"
"net"
"net/http"
"os"
"time"

"github.com/netdata/go.d.plugin/pkg/stm"
Expand All @@ -17,9 +18,6 @@ type reqErrCode int

const (
codeTimeout reqErrCode = iota
//codeDNSLookup
//codeParseAddress
//codeRedirect
codeNoConnection
)

Expand All @@ -29,13 +27,20 @@ func (hc *HTTPCheck) collect() (map[string]int64, error) {
return nil, fmt.Errorf("error on creating HTTP requests to %s : %v", hc.Request.URL, err)
}

var mx metrics
if hc.CookieFile != "" {
if err := hc.readCookieFile(); err != nil {
return nil, fmt.Errorf("error on reading cookie file '%s': %v", hc.CookieFile, err)
}
}

start := time.Now()
resp, err := hc.client.Do(req)
resp, err := hc.httpClient.Do(req)
dur := time.Since(start)

defer closeBody(resp)

var mx metrics

if err != nil {
hc.Warning(err)
hc.collectErrResponse(&mx, err)
Expand All @@ -44,35 +49,24 @@ func (hc *HTTPCheck) collect() (map[string]int64, error) {
hc.collectOKResponse(&mx, resp)
}

changed := hc.metrics.Status != mx.Status
if changed {
if hc.metrics.Status != mx.Status {
mx.InState = hc.UpdateEvery
} else {
mx.InState = hc.metrics.InState + hc.UpdateEvery
}
hc.metrics = mx

//if err == nil || mx.Status.RedirectError {
// mx.ResponseTime = durationToMs(end)
//}

return stm.ToMap(mx), nil
}

func (hc *HTTPCheck) collectErrResponse(mx *metrics, err error) {
switch code := decodeReqError(err); code {
default:
panic(fmt.Sprintf("unknown request error code : %d", code))
case codeNoConnection:
mx.Status.NoConnection = true
//case codeDNSLookup:
// mx.Status.DNSLookupError = true
//case codeParseAddress:
// mx.Status.ParseAddressError = true
//case codeRedirect:
// mx.Status.RedirectError = true
case codeTimeout:
mx.Status.Timeout = true
default:
panic(fmt.Sprintf("unknown request error code : %d", code))
}
}

Expand Down Expand Up @@ -109,34 +103,34 @@ func decodeReqError(err error) reqErrCode {
return codeTimeout
}
return codeNoConnection
//
//netErr, isNetErr := err.(net.Error)
//if isNetErr && netErr.Timeout() {
// return codeTimeout
//}
//
//urlErr, isURLErr := err.(*url.Error)
//if !isURLErr {
// return codeNoConnection
//}
//
//if urlErr.Err == web.ErrRedirectAttempted {
// return codeRedirect
//}
//
//opErr, isOpErr := (urlErr.Err).(*net.OpError)
//if !isOpErr {
// return codeNoConnection
//}
//
//switch (opErr.Err).(type) {
//case *net.DNSError:
// return codeDNSLookup
//case *net.ParseError:
// return codeParseAddress
//}
//
//return codeNoConnection
}

func (hc *HTTPCheck) readCookieFile() error {
if hc.CookieFile == "" {
return nil
}

fi, err := os.Stat(hc.CookieFile)
if err != nil {
return err
}

if hc.cookieFileModTime.Equal(fi.ModTime()) {
hc.Debugf("cookie file '%s' modification time has not changed, using previously read data", hc.CookieFile)
return nil
}

hc.Debugf("reading cookie file '%s'", hc.CookieFile)

jar, err := loadCookieJar(hc.CookieFile)
if err != nil {
return err
}

hc.httpClient.Jar = jar
hc.cookieFileModTime = fi.ModTime()

return nil
}

func closeBody(resp *http.Response) {
Expand Down
89 changes: 89 additions & 0 deletions modules/httpcheck/cookiejar.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// SPDX-License-Identifier: GPL-3.0-or-later

package httpcheck

import (
"bufio"
"fmt"
"net/http"
"net/http/cookiejar"
"net/url"
"os"
"strconv"
"strings"
"time"

"golang.org/x/net/publicsuffix"
)

// TODO: implement proper cookie auth support
// relevant forum topic: https://community.netdata.cloud/t/howto-http-endpoint-collector-with-cookie-and-user-pass/3981/5?u=ilyam8

// cookie file format: https://everything.curl.dev/http/cookies/fileformat
func loadCookieJar(path string) (http.CookieJar, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer func() { _ = file.Close() }()

jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
if err != nil {
return nil, err
}

sc := bufio.NewScanner(file)

for sc.Scan() {
line, httpOnly := strings.CutPrefix(strings.TrimSpace(sc.Text()), "#HttpOnly_")

if strings.HasPrefix(line, "#") || line == "" {
continue
}

parts := strings.Fields(line)
if len(parts) != 6 && len(parts) != 7 {
return nil, fmt.Errorf("got %d fields in line '%s', want 6 or 7", len(parts), line)
}

for i, v := range parts {
parts[i] = strings.TrimSpace(v)
}

cookie := &http.Cookie{
Domain: parts[0],
Path: parts[2],
Name: parts[5],
HttpOnly: httpOnly,
}
cookie.Secure, err = strconv.ParseBool(parts[3])
if err != nil {
return nil, err
}
expires, err := strconv.ParseInt(parts[4], 10, 64)
if err != nil {
return nil, err
}
if expires > 0 {
cookie.Expires = time.Unix(expires, 0)
}
if len(parts) == 7 {
cookie.Value = parts[6]
}

scheme := "http"
if cookie.Secure {
scheme = "https"
}
cookieURL := &url.URL{
Scheme: scheme,
Host: cookie.Domain,
}

cookies := jar.Cookies(cookieURL)
cookies = append(cookies, cookie)
jar.SetCookies(cookieURL, cookies)
}

return jar, nil
}
38 changes: 21 additions & 17 deletions modules/httpcheck/httpcheck.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,27 +37,27 @@ func New() *HTTPCheck {

type Config struct {
web.HTTP `yaml:",inline"`
UpdateEvery int `yaml:"update_every"`
AcceptedStatuses []int `yaml:"status_accepted"`
ResponseMatch string `yaml:"response_match"`
CookieFile string `yaml:"cookie_file"`
}

type (
HTTPCheck struct {
module.Base
Config `yaml:",inline"`
UpdateEvery int `yaml:"update_every"`
type HTTPCheck struct {
module.Base
Config `yaml:",inline"`

charts *module.Charts
httpClient *http.Client

acceptedStatuses map[int]bool
reResponse *regexp.Regexp
client client
metrics metrics
}
client interface {
Do(*http.Request) (*http.Response, error)
}
)
charts *module.Charts

acceptedStatuses map[int]bool
reResponse *regexp.Regexp

cookieFileModTime time.Time

metrics metrics
}

func (hc *HTTPCheck) Init() bool {
if err := hc.validateConfig(); err != nil {
Expand All @@ -72,7 +72,7 @@ func (hc *HTTPCheck) Init() bool {
hc.Errorf("init HTTP client: %v", err)
return false
}
hc.client = httpClient
hc.httpClient = httpClient

re, err := hc.initResponseMatchRegexp()
if err != nil {
Expand Down Expand Up @@ -115,4 +115,8 @@ func (hc *HTTPCheck) Collect() map[string]int64 {
return mx
}

func (hc *HTTPCheck) Cleanup() {}
func (hc *HTTPCheck) Cleanup() {
if hc.httpClient != nil {
hc.httpClient.CloseIdleConnections()
}
}
Loading

0 comments on commit 0f6efe0

Please sign in to comment.