Skip to content

Commit

Permalink
fix(inputs.http_response): Fix for IPv4 and IPv6 addresses when inter…
Browse files Browse the repository at this point in the history
…face is set
  • Loading branch information
zak-pawel committed Jun 11, 2024
1 parent 7dbe28d commit 0a48e20
Show file tree
Hide file tree
Showing 5 changed files with 270 additions and 121 deletions.
2 changes: 2 additions & 0 deletions docs/LICENSE_OF_DEPENDENCIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,8 @@ following works:
- github.com/russross/blackfriday [BSD 2-Clause "Simplified" License](https://github.com/russross/blackfriday/blob/master/LICENSE.txt)
- github.com/safchain/ethtool [Apache License 2.0](https://github.com/safchain/ethtool/blob/master/LICENSE)
- github.com/samber/lo [MIT License](https://github.com/samber/lo/blob/master/LICENSE)
- github.com/seancfoley/bintree [Apache License 2.0](https://github.com/seancfoley/bintree/blob/master/LICENSE)
- github.com/seancfoley/ipaddress-go [Apache License 2.0](https://github.com/seancfoley/ipaddress-go/blob/master/LICENSE)
- github.com/shirou/gopsutil [BSD 3-Clause Clear License](https://github.com/shirou/gopsutil/blob/master/LICENSE)
- github.com/shoenig/go-m1cpu [Mozilla Public License 2.0](https://github.com/shoenig/go-m1cpu/blob/main/LICENSE)
- github.com/shopspring/decimal [MIT License](https://github.com/shopspring/decimal/blob/master/LICENSE)
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ require (
github.com/robinson/gos7 v0.0.0-20240315073918-1f14519e4846
github.com/safchain/ethtool v0.3.0
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
github.com/seancfoley/ipaddress-go v1.6.0
github.com/sensu/sensu-go/api/core/v2 v2.16.0
github.com/shirou/gopsutil/v3 v3.24.4
github.com/showwin/speedtest-go v1.7.7
Expand Down Expand Up @@ -436,6 +437,7 @@ require (
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/samber/lo v1.38.1 // indirect
github.com/seancfoley/bintree v1.3.1 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/signalfx/com_signalfx_metrics_protobuf v0.0.3 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2111,6 +2111,10 @@ github.com/scaleway/scaleway-sdk-go v1.0.0-beta.21 h1:yWfiTPwYxB0l5fGMhl/G+liULu
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.21/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/seancfoley/bintree v1.3.1 h1:cqmmQK7Jm4aw8gna0bP+huu5leVOgHGSJBEpUx3EXGI=
github.com/seancfoley/bintree v1.3.1/go.mod h1:hIUabL8OFYyFVTQ6azeajbopogQc2l5C/hiXMcemWNU=
github.com/seancfoley/ipaddress-go v1.6.0 h1:9z7yGmOnV4P2ML/dlR/kCJiv5tp8iHOOetJvxJh/R5w=
github.com/seancfoley/ipaddress-go v1.6.0/go.mod h1:TQRZgv+9jdvzHmKoPGBMxyiaVmoI0rYpfEk8Q/sL/Iw=
github.com/sensu/sensu-go/api/core/v2 v2.16.0 h1:HOq4rFkQ1S5ZjxmMTLc5J5mAbECrnKWvtXXbMqr3j9s=
github.com/sensu/sensu-go/api/core/v2 v2.16.0/go.mod h1:MjM7+MCGEyTAgaZ589SiGHwYiaYF7N/58dU0J070u/0=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
Expand Down
136 changes: 90 additions & 46 deletions plugins/inputs/http_response/http_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ import (
"time"
"unicode/utf8"

"github.com/benbjohnson/clock"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/internal"
"github.com/influxdata/telegraf/plugins/common/cookie"
"github.com/influxdata/telegraf/plugins/common/tls"
"github.com/influxdata/telegraf/plugins/inputs"

"github.com/benbjohnson/clock"
"github.com/seancfoley/ipaddress-go/ipaddr"
)

//go:embed sample.conf
Expand All @@ -36,40 +38,45 @@ const (

// HTTPResponse struct
type HTTPResponse struct {
Address string `toml:"address" deprecated:"1.12.0;1.35.0;use 'urls' instead"`
URLs []string `toml:"urls"`
HTTPProxy string `toml:"http_proxy"`
Body string
Address string `toml:"address" deprecated:"1.12.0;1.35.0;use 'urls' instead"`
URLs []string `toml:"urls"`
HTTPProxy string `toml:"http_proxy"`
Body string `toml:"body"`
BodyForm map[string][]string `toml:"body_form"`
Method string
ResponseTimeout config.Duration
HTTPHeaderTags map[string]string `toml:"http_header_tags"`
Headers map[string]string
FollowRedirects bool
Method string `toml:"method"`
ResponseTimeout config.Duration `toml:"response_timeout"`
HTTPHeaderTags map[string]string `toml:"http_header_tags"`
Headers map[string]string `toml:"headers"`
FollowRedirects bool `toml:"follow_redirects"`
// Absolute path to file with Bearer token
BearerToken string `toml:"bearer_token"`
ResponseBodyField string `toml:"response_body_field"`
ResponseBodyMaxSize config.Size `toml:"response_body_max_size"`
ResponseStringMatch string
ResponseStatusCode int
Interface string
ResponseStringMatch string `toml:"response_string_match"`
ResponseStatusCode int `toml:"response_status_code"`
Interface string `toml:"interface"`
// HTTP Basic Auth Credentials
Username config.Secret `toml:"username"`
Password config.Secret `toml:"password"`
tls.ClientConfig
cookie.CookieAuthConfig

Log telegraf.Logger
Log telegraf.Logger `toml:"-"`

compiledStringMatch *regexp.Regexp
client httpClient
clients []client
}

type client struct {
httpClient httpClient
address string
}

type httpClient interface {
Do(req *http.Request) (*http.Response, error)
}

// Set the proxy. A configured proxy overwrites the system wide proxy.
// Set the proxy. A configured proxy overwrites the system-wide proxy.
func getProxyFunc(httpProxy string) func(*http.Request) (*url.URL, error) {
if httpProxy == "" {
return http.ProxyFromEnvironment
Expand All @@ -85,9 +92,9 @@ func getProxyFunc(httpProxy string) func(*http.Request) (*url.URL, error) {
}
}

// createHTTPClient creates an http client which will timeout at the specified
// createHTTPClient creates an http client which will time out at the specified
// timeout period and can follow redirects if specified
func (h *HTTPResponse) createHTTPClient() (*http.Client, error) {
func (h *HTTPResponse) createHTTPClient(address url.URL) (*http.Client, error) {
tlsCfg, err := h.ClientConfig.TLSConfig()
if err != nil {
return nil, err
Expand All @@ -96,7 +103,7 @@ func (h *HTTPResponse) createHTTPClient() (*http.Client, error) {
dialer := &net.Dialer{}

if h.Interface != "" {
dialer.LocalAddr, err = localAddress(h.Interface)
dialer.LocalAddr, err = localAddress(h.Interface, address)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -127,7 +134,7 @@ func (h *HTTPResponse) createHTTPClient() (*http.Client, error) {
return client, nil
}

func localAddress(interfaceName string) (net.Addr, error) {
func localAddress(interfaceName string, address url.URL) (net.Addr, error) {
i, err := net.InterfaceByName(interfaceName)
if err != nil {
return nil, err
Expand All @@ -138,14 +145,48 @@ func localAddress(interfaceName string) (net.Addr, error) {
return nil, err
}

urlInIPv6 := isURLInIPv6(address)
for _, addr := range addrs {
if naddr, ok := addr.(*net.IPNet); ok {
// leaving port set to zero to let kernel pick
return &net.TCPAddr{IP: naddr.IP}, nil
ipNetInIPv6 := isIPNetInIPv6(naddr)

// choose interface address in the same format as server address
if ipNetInIPv6 == urlInIPv6 {
// leaving port set to zero to let kernel pick, but set zone for IPv6 format
if urlInIPv6 {
return &net.TCPAddr{IP: naddr.IP, Zone: interfaceName}, nil
}
return &net.TCPAddr{IP: naddr.IP}, nil
}
}
}

return nil, fmt.Errorf("cannot create local address for interface %q", interfaceName)
return nil, fmt.Errorf("cannot create local address for interface %q and server address %q", interfaceName, address.String())
}

// isURLInIPv6 returns true only when URL is in IPv6 format.
// For other cases (host part of url cannot be successfully validated, doesn't contain address at all or is in IPv4 format), it returns false.
func isURLInIPv6(address url.URL) bool {
host := ipaddr.NewHostName(address.Host)
err := host.Validate()
if err == nil {
if host.IsAddress() {
return host.AsAddress().IsIPv6()
}
}

return false
}

// isIPNetInIPv6 returns true only when IPNet can be represented in IPv6 format.
// For other cases (address cannot be successfully parsed or is in IPv4 format), it returns false.
func isIPNetInIPv6(address *net.IPNet) bool {
ipAddr, err := ipaddr.NewIPAddressFromNetIPNet(address)
if err == nil {
return ipAddr.IsIPv6()
}

return false
}

func setResult(resultString string, fields map[string]interface{}, tags map[string]string) {
Expand Down Expand Up @@ -196,10 +237,10 @@ func setError(err error, fields map[string]interface{}, tags map[string]string)
}

// HTTPGather gathers all fields and returns any errors it encounters
func (h *HTTPResponse) httpGather(u string) (map[string]interface{}, map[string]string, error) {
func (h *HTTPResponse) httpGather(cl client) (map[string]interface{}, map[string]string, error) {
// Prepare fields and tags
fields := make(map[string]interface{})
tags := map[string]string{"server": u, "method": h.Method}
tags := map[string]string{"server": cl.address, "method": h.Method}

var body io.Reader
if h.Body != "" {
Expand All @@ -214,7 +255,7 @@ func (h *HTTPResponse) httpGather(u string) (map[string]interface{}, map[string]
body = strings.NewReader(values.Encode())
}

request, err := http.NewRequest(h.Method, u, body)
request, err := http.NewRequest(h.Method, cl.address, body)
if err != nil {
return nil, nil, err
}
Expand Down Expand Up @@ -245,14 +286,14 @@ func (h *HTTPResponse) httpGather(u string) (map[string]interface{}, map[string]

// Start Timer
start := time.Now()
resp, err := h.client.Do(request)
resp, err := cl.httpClient.Do(request)
responseTime := time.Since(start).Seconds()

// If an error in returned, it means we are dealing with a network error, as
// HTTP error codes do not generate errors in the net/http library
if err != nil {
// Log error
h.Log.Debugf("Network error while polling %s: %s", u, err.Error())
h.Log.Debugf("Network error while polling %s: %s", cl.address, err.Error())

// Get error details
if setError(err, fields, tags) == nil {
Expand Down Expand Up @@ -352,10 +393,9 @@ func (*HTTPResponse) SampleConfig() string {
return sampleConfig
}

// Gather gets all metric fields and tags and returns any errors it encounters
func (h *HTTPResponse) Gather(acc telegraf.Accumulator) error {
// Compile the body regex if it exist
if h.compiledStringMatch == nil {
func (h *HTTPResponse) Init() error {
// Compile the body regex if it exists
if h.ResponseStringMatch != "" {
var err error
h.compiledStringMatch, err = regexp.Compile(h.ResponseStringMatch)
if err != nil {
Expand All @@ -367,7 +407,6 @@ func (h *HTTPResponse) Gather(acc telegraf.Accumulator) error {
if h.ResponseTimeout < config.Duration(time.Second) {
h.ResponseTimeout = config.Duration(time.Second * 5)
}
// Check send and expected string
if h.Method == "" {
h.Method = "GET"
}
Expand All @@ -380,32 +419,37 @@ func (h *HTTPResponse) Gather(acc telegraf.Accumulator) error {
}
}

if h.client == nil {
client, err := h.createHTTPClient()
if err != nil {
return err
}
h.client = client
}

h.clients = make([]client, 0, len(h.URLs))
for _, u := range h.URLs {
addr, err := url.Parse(u)
if err != nil {
acc.AddError(err)
continue
return fmt.Errorf("%q is not a valid address: %w", u, err)
}

if addr.Scheme != "http" && addr.Scheme != "https" {
acc.AddError(errors.New("only http and https are supported"))
continue
return fmt.Errorf("%q is not a valid address: only http and https types are supported", u)
}

cl, err := h.createHTTPClient(*addr)
if err != nil {
return err
}

h.clients = append(h.clients, client{httpClient: cl, address: u})
}

return nil
}

// Gather gets all metric fields and tags and returns any errors it encounters
func (h *HTTPResponse) Gather(acc telegraf.Accumulator) error {
for _, c := range h.clients {
// Prepare data
var fields map[string]interface{}
var tags map[string]string

// Gather data
fields, tags, err = h.httpGather(u)
fields, tags, err := h.httpGather(c)
if err != nil {
acc.AddError(err)
continue
Expand Down
Loading

0 comments on commit 0a48e20

Please sign in to comment.