Skip to content

Commit

Permalink
Merge 85d06a3 into 093bef0
Browse files Browse the repository at this point in the history
  • Loading branch information
astj committed Nov 22, 2018
2 parents 093bef0 + 85d06a3 commit 62415c3
Show file tree
Hide file tree
Showing 3 changed files with 204 additions and 7 deletions.
9 changes: 8 additions & 1 deletion check-http/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,11 @@ check-http -s 404=ok -u http://example.com
check-http -s 200-404=ok -u http://example.com
```


To change request destination
```shell
check-http --connect-to=example.com:443:127.0.0.1:8080 https://example.com # will request to 127.0.0.1:8000 but AS example.com:443
check-http --connect-to=:443:127.0.0.1:8080 https://example.com # empty host1 matches ANY host
check-http --connect-to=example.com::127.0.0.1:8080 https://example.com # empty port1 matches ANY port
check-http --connect-to=localhost:443::8080 https://localhost # empty host2 means unchanged, therefore will request to localhost:8080 AS localhost:443
check-http --connect-to=example.com:443:127.0.0.1: https://example.com # empty port2 means unchanged, therefore will request to 127.0.0.1:443
```
88 changes: 82 additions & 6 deletions check-http/lib/check_http.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package checkhttp
import (
"bufio"
"bytes"
"context"
"crypto/tls"
"fmt"
"io/ioutil"
Expand All @@ -28,6 +29,7 @@ type checkHTTPOpts struct {
Headers []string `short:"H" description:"HTTP request headers"`
Regexp string `short:"p" long:"pattern" description:"Expected pattern in the content"`
MaxRedirects int `long:"max-redirects" description:"Maximum number of redirects followed" default:"10"`
ConnectTos []string `long:"connect-to" value-name:"HOST1:PORT1:HOST2:PORT2" description:"Request to HOST2:PORT2 instead of HOST1:PORT1"`
}

// Do the plugin
Expand All @@ -45,6 +47,44 @@ type statusRange struct {

const invalidMapping = "Invalid mapping of status: %s"

// when empty:
// - src* will be treated as ANY
// - dest* will be treated as unchanged
type resolveMapping struct {
srcHost string
srcPort string
destHost string
destPort string
}

func newReplacableDial(dialer *net.Dialer, mappings []resolveMapping) func(ctx context.Context, network, addr string) (net.Conn, error) {
return func(ctx context.Context, network, hostport string) (net.Conn, error) {
host, port, err := net.SplitHostPort(hostport)
if err != nil {
return nil, err
}

addr := hostport
for _, m := range mappings {
if m.srcHost != "" && m.srcHost != host {
continue
}
if m.srcPort != "" && m.srcPort != port {
continue
}
if m.destHost != "" {
host = m.destHost
}
if m.destPort != "" {
port = m.destPort
}
addr = net.JoinHostPort(host, port)
break
}
return dialer.DialContext(ctx, network, addr)
}
}

func parseStatusRanges(opts *checkHTTPOpts) ([]statusRange, error) {
var statuses []statusRange
for _, s := range opts.Statuses {
Expand Down Expand Up @@ -107,6 +147,33 @@ func parseHeader(opts *checkHTTPOpts) (http.Header, error) {
return http.Header(mimeheader), nil
}

var connectToRegexp = regexp.MustCompile(`^(\[.+\]|[^\[\]]+)?:(\d*):(\[.+\]|[^\[\]]+)?:(\d+)?$`)

func parseConnectTo(opts *checkHTTPOpts) ([]resolveMapping, error) {
mappings := make([]resolveMapping, len(opts.ConnectTos))
for i, c := range opts.ConnectTos {
s := connectToRegexp.FindStringSubmatch(c)
if len(s) == 0 {
return nil, fmt.Errorf("Invalid --connect-to pattern: %s", c)
}
r := resolveMapping{}
if len(s) >= 2 {
r.srcHost = s[1]
}
if len(s) >= 3 {
r.srcPort = s[2]
}
if len(s) >= 4 {
r.destHost = s[3]
}
if len(s) >= 5 {
r.destPort = s[4]
}
mappings[i] = r
}
return mappings, nil
}

// Run do external monitoring via HTTP
func Run(args []string) *checkers.Checker {
opts := checkHTTPOpts{}
Expand All @@ -126,17 +193,26 @@ func Run(args []string) *checkers.Checker {
},
Proxy: http.ProxyFromEnvironment,
}
// same as http.Transport's default dialer
dialer := &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}
if opts.SourceIP != "" {
ip := net.ParseIP(opts.SourceIP)
if ip == nil {
return checkers.Unknown(fmt.Sprintf("Invalid source IP address: %v", opts.SourceIP))
}
tr.Dial = (&net.Dialer{
LocalAddr: &net.TCPAddr{IP: ip},
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).Dial
dialer.LocalAddr = &net.TCPAddr{IP: ip}
}

if len(opts.ConnectTos) != 0 {
resolves, err := parseConnectTo(&opts)
if err != nil {
return checkers.Unknown(err.Error())
}
tr.DialContext = newReplacableDial(dialer, resolves)
}
client := &http.Client{Transport: tr}
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
Expand Down
114 changes: 114 additions & 0 deletions check-http/lib/check_http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/mackerelio/checkers"
Expand Down Expand Up @@ -169,3 +170,116 @@ func TestMaxRedirects(t *testing.T) {
assert.Equal(t, ckr.Status, tc.want, "#%d: Status should be %s", i, tc.want)
}
}

func TestConnectTos(t *testing.T) {
// expected server
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "this is %s\n", r.URL.Host)
}))
defer ts.Close()
// extract host and port
s := strings.SplitN(ts.URL, ":", 3)
addr := strings.TrimPrefix(s[1], "//")
port := s[2]

// NON-expected server
ts2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "wrong server!", 500)
}))
defer ts2.Close()
// extract host and port
s2 := strings.SplitN(ts.URL, ":", 3)
addr2 := strings.TrimPrefix(s2[1], "//")
port2 := s2[2]

testCases := []struct {
args []string
want checkers.Status
}{
{
// not affected at all
args: []string{"--connect-to", fmt.Sprintf("hoge:80:%s:%s", addr, port),
"-u", ts.URL},
want: checkers.OK,
},
{
// connected to target
args: []string{"--connect-to", fmt.Sprintf("hoge:80:%s:%s", addr, port),
"-u", "http://hoge"},
want: checkers.OK,
},
{
// empty srcHost means ANY
args: []string{"--connect-to", fmt.Sprintf(":80:%s:%s", addr, port),
"-u", "http://hoge"},
want: checkers.OK,
},
{
// empty srcPort means ANY
args: []string{"--connect-to", fmt.Sprintf("hoge::%s:%s", addr, port),
"-u", "http://hoge"},
want: checkers.OK,
},
{
// empty destHost means unchanged
args: []string{"--connect-to", fmt.Sprintf("%s:80::%s", addr, port),
"-u", fmt.Sprintf("http://%s", addr)},
want: checkers.OK,
},
{
// empty destPort means unchanged
args: []string{"--connect-to", fmt.Sprintf("hoge:%s:%s:", port, addr),
"-u", fmt.Sprintf("http://hoge:%s", port)},
want: checkers.OK,
},
{
// host mismatch ignored
args: []string{"--connect-to", fmt.Sprintf("not.target:%s:%s:%s", port, addr2, port2),
"-u", ts.URL},
want: checkers.OK,
},
{
// port mismatch ignored
args: []string{"--connect-to", fmt.Sprintf("%s:%s:%s:%s", addr, port2, addr2, port2),
"-u", ts.URL},
want: checkers.OK,
},
{
// host mismatch ignored, even if port is empty
args: []string{"--connect-to", fmt.Sprintf("not.target::%s:%s", addr2, port2),
"-u", ts.URL},
want: checkers.OK,
},
{
// port mismatch ignored, even if host is empty
args: []string{"--connect-to", fmt.Sprintf(":%s:%s:%s", port2, addr2, port2),
"-u", ts.URL},
want: checkers.OK,
},
{
// multiple setting (1)
args: []string{"--connect-to", fmt.Sprintf("not.hoge:80:%s:%s", addr2, port2),
"--connect-to", fmt.Sprintf("hoge:80:%s:%s", addr, port),
"-u", "http://hoge"},
want: checkers.OK,
},
{
// multiple setting (2)
args: []string{"--connect-to", fmt.Sprintf("hoge:80:%s:%s", addr, port),
"--connect-to", fmt.Sprintf("hoge:80:%s:%s", addr2, port2),
"-u", "http://hoge"},
want: checkers.OK,
},
{
// Invalid pattern
args: []string{"--connect-to", "foo:123:",
"-u", ts.URL},
want: checkers.UNKNOWN,
},
}

for i, tc := range testCases {
ckr := Run(tc.args)
assert.Equal(t, ckr.Status, tc.want, "#%d: Status should be %s, %s", i, tc.want, ckr.Message)
}
}

0 comments on commit 62415c3

Please sign in to comment.