diff --git a/Makefile b/Makefile index 1cb02cd..0cb0a6a 100644 --- a/Makefile +++ b/Makefile @@ -1,18 +1,19 @@ VERSION=0.0.24 -LDFLAGS=-ldflags "-w -s -X main.version=${VERSION} " +GITCOMMIT?=$(shell git describe --dirty --always) +LDFLAGS=-ldflags "-w -s -X main.version=${VERSION} -X main.commit=${GITCOMMIT}" all: check_http2 -.PHONY: check_http2 +.PHONY: check_http2 linux check lint -check_http2: writer.go checker.go main.go - go build $(LDFLAGS) -o check_http2 writer.go checker.go main.go +check_http2: *.go + go build $(LDFLAGS) -o check_http2 -linux: writer.go checker.go main.go - GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o check_http2 writer.go checker.go main.go +linux: *.go + GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o check_http2 check: go test -v ./... -fmt: - go fmt ./... +lint: + golangci-lint run ./... diff --git a/checker.go b/checker.go index ee20268..c61c35a 100644 --- a/checker.go +++ b/checker.go @@ -159,8 +159,8 @@ func (opt *Opt) BuildRequest(ctx context.Context) (*http.Request, error) { } func (opt *Opt) ExpectedStatusCode(status string) string { - expects := strings.Split(opt.Expect, ",") - for _, e := range expects { + expects := strings.SplitSeq(opt.Expect, ",") + for e := range expects { if strings.HasPrefix(status, e) { return e } @@ -227,8 +227,8 @@ func (opt *Opt) Request(ctx context.Context, client *http.Client) (string, *Requ } } - b.Write([]byte(statusLine + "\r\n\r\n")) - res.Header.Write(b) + _, _ = b.Write([]byte(statusLine + "\r\n\r\n")) + _ = res.Header.Write(b) okMsg := fmt.Sprintf(`HTTP OK: %s - %d bytes in %.3f second response time | time=%fs;;;0.000000 size=%dB;;;0`, strings.Join(matched, ", "), b.Size(), duration.Seconds(), duration.Seconds(), b.Size()) return okMsg, nil diff --git a/main.go b/main.go index 9bda654..5011b8f 100644 --- a/main.go +++ b/main.go @@ -4,10 +4,10 @@ import ( "context" "encoding/base64" "fmt" - "log" "net" "net/http" "os" + "path/filepath" "runtime" "strconv" "time" @@ -16,75 +16,59 @@ import ( ) var version string +var commit string -const UNKNOWN = 3 -const CRITICAL = 2 -const WARNING = 1 -const OK = 0 - -func printVersion() { - fmt.Printf(`%s Compiler: %s %s`, - version, - runtime.Compiler, - runtime.Version()) -} - -func main() { - os.Exit(_main()) -} - -func _main() int { - opt := Opt{} - psr := flags.NewParser(&opt, flags.Default) - _, err := psr.Parse() - if err != nil { - os.Exit(UNKNOWN) - } - - if opt.Version { - printVersion() - return OK - } - - opt.bufferSize = uint64(opt.MaxBufferSize) +const ( + OK = iota + WARNING + CRITICAL + UNKNOWN +) +func (opt *Opt) verifyWaitFor() error { if opt.WaitFor && opt.WaitForMax == 0 { - fmt.Printf("wait-for-max is required when wait-for is enabled\n") - return UNKNOWN + return fmt.Errorf("wait-for-max is required when wait-for is enabled") } + return nil +} +func (opt *Opt) verifyExpectedContent() error { if opt.ExpectContent != "" && opt.Base64ExpectContent != "" { - fmt.Printf("Both string and base64-string are specified\n") - return UNKNOWN + return fmt.Errorf("both string and base64-string are specified") } if opt.ExpectContent != "" { opt.expectByte = []byte(opt.ExpectContent) } + if opt.Base64ExpectContent != "" { data, err := base64.StdEncoding.DecodeString(opt.Base64ExpectContent) if err != nil { - fmt.Printf("Failed decode base64-string: %v\n", err) - return UNKNOWN + return fmt.Errorf("failed decode base64-string: %w", err) } opt.expectByte = data } + return nil +} + +func (opt *Opt) verifyHostOptions() error { if opt.TCP4 && opt.TCP6 { - fmt.Printf("Both tcp4 and tcp6 are specified\n") - return UNKNOWN + return fmt.Errorf("both tcp4 and tcp6 are specified") } if opt.SNI && opt.Hostname == "" { - fmt.Printf("hostname is required when use sni\n") - return UNKNOWN + return fmt.Errorf("hostname is required when using sni") } if opt.Hostname == "" && opt.IPAddress == "" { - fmt.Printf("Specify either hostname or ipaddress\n") - return UNKNOWN + return fmt.Errorf("specify either hostname or ipaddress") } + return nil +} + +func (opt *Opt) normalizeHostAndIP() { if opt.Hostname == "" { opt.Hostname = opt.IPAddress } @@ -93,11 +77,13 @@ func _main() int { host, _, err := net.SplitHostPort(opt.Hostname) if err != nil { opt.IPAddress = opt.Hostname - } else { - opt.IPAddress = host + return } + opt.IPAddress = host } +} +func (opt *Opt) setDefaultPort() { if opt.Port == 0 { _, port, err := net.SplitHostPort(opt.Hostname) if err == nil { @@ -114,11 +100,37 @@ func _main() int { opt.Port = 80 } } +} +func (opt *Opt) setDefaultURI() { if opt.URI == "" { opt.URI = "/" } +} + +func (opt *Opt) verify() error { + opt.bufferSize = uint64(opt.MaxBufferSize) + + if err := opt.verifyWaitFor(); err != nil { + return err + } + + if err := opt.verifyExpectedContent(); err != nil { + return err + } + if err := opt.verifyHostOptions(); err != nil { + return err + } + + opt.normalizeHostAndIP() + opt.setDefaultPort() + opt.setDefaultURI() + + return nil +} + +func (opt *Opt) BuildClient() *http.Client { transport := opt.MakeTransport() client := &http.Client{ Transport: transport, @@ -127,6 +139,11 @@ func _main() int { }, Timeout: opt.Timeout, } + return client +} + +func (opt *Opt) run() int { + client := opt.BuildClient() ctx := context.Background() timeout := opt.Timeout + 3*time.Second @@ -136,59 +153,48 @@ func _main() int { ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() - requestNum := 0 if opt.WaitFor { - consecutive := opt.Consecutive - 1 - for ctx.Err() == nil { - requestNum++ - okMsg, reqErr := opt.Request(ctx, client) - interval := opt.Interim - if reqErr == nil && consecutive <= 0 { - log.Printf("request[%d]: %s", requestNum, okMsg) - fmt.Println(okMsg) - return OK - } else if reqErr == nil { - consecutive-- - log.Printf("request[%d]: %s", requestNum, okMsg) - } else { - interval = opt.WaitForInterval - consecutive = opt.Consecutive - 1 - log.Printf("request[%d]: %s", requestNum, reqErr.Error()) - } - select { - case <-ctx.Done(): - case <-time.After(interval): - } - } - fmt.Printf("Give up waiting for success\n") - return UNKNOWN + msg, code := opt.runWaitFor(ctx, client) + fmt.Println(msg) + return code } - consecutive := opt.Consecutive - 1 - var rErr *RequestError - for ctx.Err() == nil { - var okMsg string - requestNum++ - okMsg, rErr = opt.Request(ctx, client) - if rErr == nil && consecutive <= 0 { - log.Printf("request[%d]: %s", requestNum, okMsg) - fmt.Println(okMsg) - return OK - } else if rErr == nil { - consecutive-- - log.Printf("request[%d]: %s", requestNum, okMsg) - } else { - break - } - select { - case <-ctx.Done(): - case <-time.After(opt.Interim): + msg, code := opt.runRequest(ctx, client) + fmt.Println(msg) + return code +} + +func main() { + os.Exit(_main()) +} + +func _main() int { + opt := &Opt{} + psr := flags.NewParser(opt, flags.HelpFlag|flags.PassDoubleDash) + _, err := psr.Parse() + if opt.Version { + if commit == "" { + commit = "dev" } + fmt.Printf( + "%s-%s\n%s/%s, %s, %s\n", + filepath.Base(os.Args[0]), + version, + runtime.GOOS, + runtime.GOARCH, + runtime.Version(), + commit) + return OK + } else if flags.WroteHelp(err) { + fmt.Fprintf(os.Stdout, "%v\n", err) + return OK + } else if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + return UNKNOWN } - if rErr == nil { - fmt.Println("HTTP UNKNOWN - timeout") + if err := opt.verify(); err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) return UNKNOWN } - fmt.Println(rErr.Error()) - return rErr.Code() + return opt.run() } diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..67f2c4b --- /dev/null +++ b/main_test.go @@ -0,0 +1,134 @@ +package main + +import "testing" + +func TestVerifyBufferSize(t *testing.T) { + opt := Opt{Hostname: "example.com", MaxBufferSize: HumanBytes(2048)} + if err := opt.verify(); err != nil { + t.Fatalf("verify() error = %v", err) + } + if opt.bufferSize != 2048 { + t.Fatalf("bufferSize = %d, want 2048", opt.bufferSize) + } +} + +func TestVerifyWaitForWithoutMax(t *testing.T) { + opt := Opt{WaitFor: true, Hostname: "example.com"} + if err := opt.verify(); err == nil { + t.Fatal("verify() error = nil, want error") + } +} + +func TestVerifyExpectContentSetsExpectByte(t *testing.T) { + opt := Opt{Hostname: "example.com", ExpectContent: "hello"} + if err := opt.verify(); err != nil { + t.Fatalf("verify() error = %v", err) + } + if string(opt.expectByte) != "hello" { + t.Fatalf("expectByte = %q, want %q", opt.expectByte, "hello") + } +} + +func TestVerifyBase64ExpectContent(t *testing.T) { + opt := Opt{Hostname: "example.com", Base64ExpectContent: "aGVsbG8="} + if err := opt.verify(); err != nil { + t.Fatalf("verify() error = %v", err) + } + if string(opt.expectByte) != "hello" { + t.Fatalf("expectByte = %q, want %q", opt.expectByte, "hello") + } +} + +func TestVerifyBase64ExpectContentInvalid(t *testing.T) { + opt := Opt{Hostname: "example.com", Base64ExpectContent: "!!!"} + if err := opt.verify(); err == nil { + t.Fatal("verify() error = nil, want error") + } +} + +func TestVerifyBothExpectContents(t *testing.T) { + opt := Opt{Hostname: "example.com", ExpectContent: "plain", Base64ExpectContent: "aGVsbG8="} + if err := opt.verify(); err == nil { + t.Fatal("verify() error = nil, want error") + } +} + +func TestVerifyBothTCPModes(t *testing.T) { + opt := Opt{Hostname: "example.com", TCP4: true, TCP6: true} + if err := opt.verify(); err == nil { + t.Fatal("verify() error = nil, want error") + } +} + +func TestVerifySNIRequiresHostname(t *testing.T) { + opt := Opt{SNI: true, IPAddress: "127.0.0.1"} + if err := opt.verify(); err == nil { + t.Fatal("verify() error = nil, want error") + } +} + +func TestVerifyNoHost(t *testing.T) { + opt := Opt{} + if err := opt.verify(); err == nil { + t.Fatal("verify() error = nil, want error") + } +} + +func TestVerifyDefaults(t *testing.T) { + opt := Opt{Hostname: "example.com"} + if err := opt.verify(); err != nil { + t.Fatalf("verify() error = %v", err) + } + if opt.URI != "/" { + t.Fatalf("URI = %q, want %q", opt.URI, "/") + } + if opt.Port != 80 { + t.Fatalf("Port = %d, want 80", opt.Port) + } + if opt.IPAddress != "example.com" { + t.Fatalf("IPAddress = %q, want %q", opt.IPAddress, "example.com") + } +} + +func TestVerifyDefaultPortFromHostname(t *testing.T) { + opt := Opt{Hostname: "example.com:8080"} + if err := opt.verify(); err != nil { + t.Fatalf("verify() error = %v", err) + } + if opt.Port != 8080 { + t.Fatalf("Port = %d, want 8080", opt.Port) + } + if opt.IPAddress != "example.com" { + t.Fatalf("IPAddress = %q, want %q", opt.IPAddress, "example.com") + } +} + +func TestVerifySSLDefaultPort(t *testing.T) { + opt := Opt{Hostname: "example.com", SSL: true} + if err := opt.verify(); err != nil { + t.Fatalf("verify() error = %v", err) + } + if opt.Port != 443 { + t.Fatalf("Port = %d, want 443", opt.Port) + } +} + +func TestVerifyExplicitPortOverridesDefault(t *testing.T) { + opt := Opt{Hostname: "example.com:8443", Port: 9443, SSL: true} + if err := opt.verify(); err != nil { + t.Fatalf("verify() error = %v", err) + } + if opt.Port != 9443 { + t.Fatalf("Port = %d, want 9443", opt.Port) + } +} + +func TestVerifyIPAddressFallback(t *testing.T) { + opt := Opt{IPAddress: "192.0.2.1"} + if err := opt.verify(); err != nil { + t.Fatalf("verify() error = %v", err) + } + if opt.Hostname != "192.0.2.1" { + t.Fatalf("Hostname = %q, want %q", opt.Hostname, "192.0.2.1") + } +} diff --git a/request.go b/request.go new file mode 100644 index 0000000..d92623d --- /dev/null +++ b/request.go @@ -0,0 +1,70 @@ +package main + +import ( + "context" + "log" + "net/http" + "time" +) + +func (opt *Opt) runWaitFor(ctx context.Context, client *http.Client) (string, int) { + consecutive := opt.Consecutive - 1 + requestNum := 0 + for ctx.Err() == nil { + requestNum++ + okMsg, errReq := opt.Request(ctx, client) + interval := opt.Interim + if errReq == nil && consecutive <= 0 { + log.Printf("request[%d]: %s", requestNum, okMsg) + return okMsg, OK + } else if errReq == nil { + consecutive-- + log.Printf("request[%d]: %s", requestNum, okMsg) + } else { + interval = opt.WaitForInterval + consecutive = opt.Consecutive - 1 + log.Printf("request[%d]: %s", requestNum, errReq.Error()) + } + timer := time.NewTimer(interval) + select { + case <-ctx.Done(): + if !timer.Stop() { + <-timer.C + } + case <-timer.C: + } + } + return "Give up waiting for success", UNKNOWN +} + +func (opt *Opt) runRequest(ctx context.Context, client *http.Client) (string, int) { + consecutive := opt.Consecutive - 1 + requestNum := 0 + var rErr *RequestError + for ctx.Err() == nil { + var okMsg string + requestNum++ + okMsg, rErr = opt.Request(ctx, client) + if rErr == nil && consecutive <= 0 { + log.Printf("request[%d]: %s", requestNum, okMsg) + return okMsg, OK + } else if rErr == nil { + consecutive-- + log.Printf("request[%d]: %s", requestNum, okMsg) + } else { + break + } + timer := time.NewTimer(opt.Interim) + select { + case <-ctx.Done(): + if !timer.Stop() { + <-timer.C + } + case <-timer.C: + } + } + if rErr == nil { + return "HTTP UNKNOWN - timeout", UNKNOWN + } + return rErr.Error(), rErr.Code() +} diff --git a/request_test.go b/request_test.go new file mode 100644 index 0000000..e2195d8 --- /dev/null +++ b/request_test.go @@ -0,0 +1,217 @@ +package main + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" +) + +func newTestOpt(addr, expectContent string) Opt { + opt := Opt{ + Hostname: addr, + URI: "/", + Method: "GET", + Port: 80, + Expect: "HTTP/1.1 200", + bufferSize: 1024, + } + if expectContent != "" { + opt.expectByte = []byte(expectContent) + } + return opt +} + +type runRequestOptFunc func(*Opt) + +func runRequestTest(t *testing.T, handler http.HandlerFunc, modifyOpt runRequestOptFunc, wantCode int, wantSubstring string) { + t.Helper() + + server := httptest.NewServer(handler) + defer server.Close() + + opt := newTestOpt(server.Listener.Addr().String(), "") + opt.Consecutive = 1 + opt.Interim = 10 * time.Millisecond + if modifyOpt != nil { + modifyOpt(&opt) + } + + client := server.Client() + msg, code := opt.runRequest(context.Background(), client) + if code != wantCode { + t.Fatalf("runRequest() code = %d, want %d; msg = %s", code, wantCode, msg) + } + if !strings.Contains(msg, wantSubstring) { + t.Fatalf("runRequest() msg = %q, want substring %q", msg, wantSubstring) + } +} + +func TestRunRequestSuccess(t *testing.T) { + runRequestTest(t, + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = fmt.Fprint(w, "ok") + }), + nil, + OK, + "HTTP OK:", + ) +} + +func TestRunRequestStatusNotMatched(t *testing.T) { + runRequestTest(t, + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + _, _ = fmt.Fprint(w, "error") + }), + nil, + CRITICAL, + "Invalid HTTP response received", + ) +} + +func TestRunRequestContentNotMatched(t *testing.T) { + runRequestTest(t, + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = fmt.Fprint(w, "unexpected content") + }), + func(opt *Opt) { + opt.expectByte = []byte("expected body") + }, + CRITICAL, + "Not matched", + ) +} + +func TestRunRequestConsecutiveSuccess(t *testing.T) { + callCount := 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + callCount++ + w.WriteHeader(http.StatusOK) + _, _ = fmt.Fprint(w, "ok") + })) + defer server.Close() + + opt := newTestOpt(server.Listener.Addr().String(), "") + opt.Consecutive = 3 + opt.Interim = 10 * time.Millisecond + + client := server.Client() + msg, code := opt.runRequest(context.Background(), client) + if code != OK { + t.Fatalf("runRequest() code = %d, want %d; msg = %s", code, OK, msg) + } + if callCount != 3 { + t.Fatalf("server called %d times, want 3", callCount) + } + if !strings.HasPrefix(msg, "HTTP OK:") { + t.Fatalf("runRequest() msg = %q, want HTTP OK prefix", msg) + } +} + +func TestRequestClientTimeout(t *testing.T) { + block := make(chan struct{}) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + <-block + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + defer close(block) + + opt := newTestOpt(server.Listener.Addr().String(), "") + + client := server.Client() + client.Timeout = 50 * time.Millisecond + + _, errReq := opt.Request(context.Background(), client) + if errReq == nil { + t.Fatal("Request() error = nil, want non-nil") + } + if errReq.Code() != CRITICAL { + t.Fatalf("Request() code = %d, want %d", errReq.Code(), CRITICAL) + } + if !strings.Contains(errReq.Error(), "context deadline exceeded") { + t.Fatalf("Request() error = %q, want context deadline exceeded", errReq.Error()) + } +} + +func TestRunWaitForEventuallySucceeds(t *testing.T) { + callCount := 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + callCount++ + if callCount < 3 { + w.WriteHeader(http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) + _, _ = fmt.Fprint(w, "ok") + })) + defer server.Close() + + opt := newTestOpt(server.Listener.Addr().String(), "") + opt.Consecutive = 1 + opt.WaitFor = true + opt.WaitForInterval = 10 * time.Millisecond + opt.Interim = 10 * time.Millisecond + + client := server.Client() + msg, code := opt.runWaitFor(context.Background(), client) + if code != OK { + t.Fatalf("runWaitFor() code = %d, want %d; msg = %s", code, OK, msg) + } + if callCount < 3 { + t.Fatalf("server called %d times, want at least 3", callCount) + } + if !strings.HasPrefix(msg, "HTTP OK:") { + t.Fatalf("runWaitFor() msg = %q, want HTTP OK prefix", msg) + } +} + +func TestRunWaitForContextTimeout(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + })) + defer server.Close() + + opt := newTestOpt(server.Listener.Addr().String(), "") + opt.Consecutive = 1 + opt.WaitFor = true + opt.WaitForInterval = 10 * time.Millisecond + opt.Interim = 10 * time.Millisecond + + client := server.Client() + ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) + defer cancel() + + msg, code := opt.runWaitFor(ctx, client) + if code != UNKNOWN { + t.Fatalf("runWaitFor() code = %d, want %d; msg = %s", code, UNKNOWN, msg) + } + if msg != "Give up waiting for success" { + t.Fatalf("runWaitFor() msg = %q, want %q", msg, "Give up waiting for success") + } +} + +func TestRequestWithContentMatch(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = fmt.Fprint(w, "hello world") + })) + defer server.Close() + + opt := newTestOpt(server.Listener.Addr().String(), "world") + + client := server.Client() + msg, errReq := opt.Request(context.Background(), client) + if errReq != nil { + t.Fatalf("Request() error = %v", errReq) + } + if !strings.Contains(msg, "Response body matched") { + t.Fatalf("Request() msg = %q, want Response body matched", msg) + } +} diff --git a/writer.go b/writer.go index 0e94cca..4d21365 100644 --- a/writer.go +++ b/writer.go @@ -21,9 +21,7 @@ func (w *CapWriter) Write(p []byte) (int, error) { // Keep only up to Cap bytes; discard the rest. if uint64(len(w.buffer)) < w.Cap { remain := w.Cap - uint64(len(w.buffer)) - if remain > uint64(len(p)) { - remain = uint64(len(p)) - } + remain = min(remain, uint64(len(p))) w.buffer = append(w.buffer, p[:int(remain)]...) }