Skip to content

Commit

Permalink
healthcheck: add HTTP(S) check method
Browse files Browse the repository at this point in the history
Signed-off-by: ywc689 <[email protected]>
  • Loading branch information
ywc689 committed Sep 11, 2023
1 parent 474b853 commit e37b171
Show file tree
Hide file tree
Showing 2 changed files with 259 additions and 0 deletions.
182 changes: 182 additions & 0 deletions tools/healthcheck/pkg/helthcheck/http_checker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// Copyright 2023 IQiYi Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// The healthcheck package refers to the framework of "github.com/google/
// seesaw/healthcheck" heavily, with only some adaption changes for DPVS.

package hc

import (
"crypto/tls"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)

var _ CheckMethod = (*HttpChecker)(nil)

type HttpCodeRange struct {
start int // inclusive
end int // inclusive
}

// HttpChecker contains configuration specific to a HTTP(S) healthcheck.
type HttpChecker struct {
Config *CheckerConfig

Method string
Host string
Uri string
ResponseCodes []HttpCodeRange
Response string

Secure bool
TLSVerify bool
Proxy bool
}

// NewHttpChecker returns an initialised HttpChecker.
func NewHttpChecker(method, host, uri string) *HttpChecker {
if len(method) == 0 {
method = "GET"
}
if len(uri) == 0 {
uri = "/"
}
return &HttpChecker{
Method: method,
Uri: uri,
ResponseCodes: []HttpCodeRange{{200, 299}, {300, 399}, {400, 499}},
Response: "",
Secure: false,
TLSVerify: true,
Proxy: false,
}
}

func (hc *HttpChecker) BindConfig(conf *CheckerConfig) {
hc.Config = conf
if len(hc.Host) == 0 {
hc.Host = conf.Target.Addr()
}
}

// String returns the string representation of a HTTP healthcheck.
func (hc *HttpChecker) String() string {
attr := []string{hc.Method, hc.Host, hc.Uri}
if hc.Secure {
attr = append(attr, "secure")
if hc.TLSVerify {
attr = append(attr, "tls-verify")
}
}
if hc.Proxy {
attr = append(attr, "proxy")
}

return fmt.Sprintf("HTTP checker for %v [%s]", hc.Config.Id, strings.Join(attr, ", "))
}

// Check executes a HTTP healthcheck.
func (hc *HttpChecker) Check(target Target, timeout time.Duration) *Result {
var msg string
if hc.Secure {
msg = fmt.Sprintf("HTTPS %s to %s", hc.Method, hc.Host)
} else {
msg = fmt.Sprintf("HTTP %s to %s", hc.Method, hc.Host)
}

start := time.Now()
if timeout == time.Duration(0) {
timeout = DefaultCheckConfig.Timeout
}

u, err := url.Parse(hc.Uri)
if err != nil {
return NewResult(start, fmt.Sprintf("%s; url parse failed", msg), false, err)
}
if hc.Secure {
u.Scheme = "https"
} else {
u.Scheme = "http"
}
if len(u.Host) == 0 {
u.Host = hc.Host
}

proxy := (func(*http.Request) (*url.URL, error))(nil)
if hc.Proxy {
proxy = http.ProxyURL(u)
}

tlsConfig := &tls.Config{
InsecureSkipVerify: !hc.TLSVerify,
}
client := &http.Client{
Transport: &http.Transport{
Proxy: proxy,
TLSClientConfig: tlsConfig,
},
Timeout: timeout,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return errors.New("redirect not permitted")
},
}

req, err := http.NewRequest(hc.Method, hc.Uri, nil)
req.URL = u

// If we received a response we want to process it, even in the
// presence of an error - a redirect 3xx will result in both the
// response and an error being returned.
resp, err := client.Do(req)
if resp == nil {
return NewResult(start, fmt.Sprintf("%s; got no response", msg), false, err)
}
if resp.Body != nil {
defer resp.Body.Close()
}

// Check response code.
codeOk := false
for _, cr := range hc.ResponseCodes {
if resp.StatusCode >= cr.start && resp.StatusCode <= cr.end {
codeOk = true
break
}
}

// Check response body.
bodyOk := false
msg = fmt.Sprintf("%s; got %s", msg, resp.Status)
if len(hc.Response) == 0 {
bodyOk = true
} else if resp.Body != nil {
buf := make([]byte, len(hc.Response))
n, err := io.ReadFull(resp.Body, buf)
if err != nil && err != io.ErrUnexpectedEOF {
msg = fmt.Sprintf("%s; failed to read HTTP response", msg)
} else if string(buf) != hc.Response {
msg = fmt.Sprintf("%s; unexpected response - %q", msg, string(buf[0:n]))
} else {
bodyOk = true
}
}

return NewResult(start, msg, codeOk && bodyOk, nil)
}
77 changes: 77 additions & 0 deletions tools/healthcheck/pkg/helthcheck/http_checker_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright 2023 IQiYi Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// The healthcheck package refers to the framework of "github.com/google/
// seesaw/healthcheck" heavily, with only some adaption changes for DPVS.

package hc

import (
"fmt"
"net"
"strings"
"testing"
"time"

"github.com/iqiyi/dpvs/tools/healthcheck/pkg/utils"
)

var http_targets = []Target{
{net.ParseIP("192.168.88.30"), 80, utils.IPProtoTCP},
{net.ParseIP("192.168.88.30"), 443, utils.IPProtoTCP},
{net.ParseIP("2001::30"), 80, utils.IPProtoTCP},
{net.ParseIP("2001::30"), 443, utils.IPProtoTCP},
}

var http_url_targets = []string{
"http://www.baidu.com",
"https://www.baidu.com",
"http://www.iqiyi.com",
"https://www.iqiyi.com",
}

func TestHttpChecker(t *testing.T) {
for _, target := range http_targets {
checker := NewHttpChecker("", "", "")
checker.Host = target.Addr()
/*
if target.Port == 443 {
checker.Secure = true
}
*/
id := Id(target.String())
config := NewCheckerConfig(&id, checker, &target, StateUnknown,
0, 3*time.Second, 2*time.Second, 3)
result := checker.Check(target, config.Timeout)
fmt.Printf("[ HTTP ] %s ==> %v\n", target, result)
}

for _, target := range http_url_targets {
host := target[strings.Index(target, "://")+3:]
checker := NewHttpChecker("GET", target, "")
checker.Host = host
if strings.HasPrefix(target, "https") {
checker.Secure = true
}
id := Id(host)
config := NewCheckerConfig(&id, checker, &Target{}, StateUnknown,
0, 3*time.Second, 2*time.Second, 3)
result := checker.Check(Target{}, config.Timeout)
if result.Success == false {
t.Errorf("[ HTTP ] %s ==> %v\n", target, result)
} else {
fmt.Printf("[ HTTP ] %s ==> %v\n", target, result)
}
}
}

0 comments on commit e37b171

Please sign in to comment.