package main import ( "bytes" "crypto/tls" "crypto/x509" "fmt" "io/ioutil" "net" "net/http" "strconv" "strings" "time" "github.com/Azure/go-ntlmssp" ) const ( // Authentication types AUTH_NONE = 0 AUTH_BASIC = 1 AUTH_NTLM = 2 // Exit codes EXIT_OK = 0 EXIT_WARNING = 1 EXIT_CRITICAL = 2 EXIT_UNKNOWN = 3 ) // Authentication type Authentication struct { Type int User string Password string } type SSLCheck struct { Run bool DaysWarning int DaysCritical int } type ClientCert struct { ClientCertFile string PrivateKeyFile string } // Request type Request struct { Scheme string Host string IPAddress string TLS bool Port int URI string Timeout int Verbose bool SSLNoVerify bool Authentication Authentication FollowRedirects bool WarningTimeout int CriticalTimeout int NoSNI bool ClientCert ClientCert TLSRenegotiation bool } // Check params type Expected struct { StatusCodes []int BodyText string SSLCheck SSLCheck } // Lookup map for auth type names var authLookup = map[int]string{ AUTH_NONE: "none", AUTH_BASIC: "basic auth", AUTH_NTLM: "NTLM auth", } // URL getter func (r Request) GetURL() string { var host string if len(r.IPAddress) > 0 { host = r.IPAddress } else { host = r.Host } return fmt.Sprintf("%s://%s:%s%s", r.Scheme, host, strconv.Itoa(r.Port), r.URI) } // Use timeout interval func (r Request) UseTimoutInterval() bool { return r.WarningTimeout > 0 && r.CriticalTimeout > 0 && r.WarningTimeout < r.CriticalTimeout } // Status code check helper func checkStatusCode(code int, e *Expected) bool { for _, expectedCode := range e.StatusCodes { if expectedCode == code { return true } } return false } // Certificate check helper func checkCerts(certs [][]*x509.Certificate, e *Expected) (string, int) { timeNow := time.Now() checkedCerts := make(map[string]bool) for _, chain := range certs { for _, cert := range chain { if _, checked := checkedCerts[string(cert.Signature)]; checked { continue } checkedCerts[string(cert.Signature)] = true expiresIn := int(cert.NotAfter.Sub(timeNow).Hours()) if e.SSLCheck.DaysCritical > 0 && e.SSLCheck.DaysCritical*24 >= expiresIn { return fmt.Sprintf("CRITICAL - SSL cert expires in %f days", float32(expiresIn)/24), EXIT_CRITICAL } if e.SSLCheck.DaysWarning > 0 && e.SSLCheck.DaysWarning*24 >= expiresIn { return fmt.Sprintf("WARNING - SSL cert expires in %f days", float32(expiresIn)/24), EXIT_WARNING } } } return "", EXIT_OK } // TLS config factory func getTLSConfig(r *Request) (*tls.Config, error) { TLSConfig := &tls.Config{} // InsecureSkipVerify if r.SSLNoVerify { TLSConfig.InsecureSkipVerify = true } // SNI if !r.NoSNI && len(r.Host) > 0 { TLSConfig.ServerName = r.Host } // Client cert if r.ClientCert.ClientCertFile != "" && r.ClientCert.PrivateKeyFile != "" { cert, err := tls.LoadX509KeyPair(r.ClientCert.ClientCertFile, r.ClientCert.PrivateKeyFile) if err != nil { return nil, err } TLSConfig.Certificates = []tls.Certificate{cert} } // Renegotiation if r.TLSRenegotiation { TLSConfig.Renegotiation = tls.RenegotiateOnceAsClient } return TLSConfig, nil } // HTTP client factory func initHTTPClient(r *Request) (*http.Client, error) { // Get TLS config TLSConfig, err := getTLSConfig(r) if err != nil { return nil, err } http.DefaultTransport.(*http.Transport).TLSClientConfig = TLSConfig // Setup timeout var timeout time.Duration if r.UseTimoutInterval() { timeout = time.Duration(time.Duration(r.CriticalTimeout) * time.Second) } else { timeout = time.Duration(time.Duration(r.Timeout) * time.Second) } // Init client client := &http.Client{ Timeout: timeout, CheckRedirect: func(req *http.Request, via []*http.Request) error { if r.FollowRedirects { return nil } else { return http.ErrUseLastResponse } }, } return client, nil } // Adds custom User-Agent header func setUserAgent(request *http.Request) { request.Header.Set("User-Agent", fmt.Sprintf("icinga-http-check/%s Go-http-client/%s", appVersion, goVersion)) } // Main check function func Check(r *Request, e *Expected) (string, int, error) { if len(r.Host) == 0 && len(r.IPAddress) == 0 { return "UNKNOWN - No host or IP address given", EXIT_UNKNOWN, nil } client, err := initHTTPClient(r) if err != nil { return "CRITICAL", EXIT_CRITICAL, err } url := r.GetURL() if r.Verbose { fmt.Println(">> URL: " + url) } // Prepare request request, err := http.NewRequest("GET", url, nil) if err != nil { if r.Verbose { fmt.Println(fmt.Sprintf(">> http.NewRequest error: %v", err)) } return "UNKNOWN", EXIT_UNKNOWN, err } // User agent setUserAgent(request) // Authentication if r.Authentication.Type == AUTH_BASIC { request.SetBasicAuth(r.Authentication.User, r.Authentication.Password) } // TODO - test if r.Authentication.Type == AUTH_NTLM { // Get TLS config TLSConfig, err := getTLSConfig(r) if err != nil { return "CRITICAL", EXIT_CRITICAL, err } transport := ntlmssp.Negotiator{ RoundTripper: &http.Transport{ TLSClientConfig: TLSConfig, }, } client.Transport = transport request.SetBasicAuth(r.Authentication.User, r.Authentication.Password) } // SNI if !r.NoSNI && len(r.Host) > 0 { request.Host = r.Host } start := time.Now() timeInfo := func() string { return fmt.Sprintf("time=%fs", float32(time.Now().UnixNano()-start.UnixNano())/float32(1000000000)) } res, err := client.Do(request) if err != nil { if r.Verbose { fmt.Println(fmt.Sprintf(">> client.GET error: %v", err)) } if err, ok := err.(net.Error); ok && err.Timeout() { var timeout int if r.UseTimoutInterval() { timeout = r.CriticalTimeout } else { timeout = r.Timeout } return fmt.Sprintf("CRITICAL - Timeout - No response recieved in %d seconds|%s", timeout, timeInfo()), EXIT_CRITICAL, nil } return fmt.Sprintf("CRITICAL - %s|%s", err.Error(), timeInfo()), EXIT_CRITICAL, nil } defer res.Body.Close() // Timeout interval if r.UseTimoutInterval() { delta := float32(time.Now().UnixNano()-start.UnixNano()) / float32(1000000000) if delta >= float32(r.WarningTimeout) { return fmt.Sprintf("WARNING - Timeout - No response recieved in %d seconds|%s", r.WarningTimeout, timeInfo()), EXIT_WARNING, nil } } if r.Verbose { fmt.Println(fmt.Sprintf(">> Response status: %s", res.Status)) } // Check status code if !checkStatusCode(res.StatusCode, e) { var expectedStatusCodes []string for _, code := range e.StatusCodes { expectedStatusCodes = append(expectedStatusCodes, strconv.Itoa(code)) } return fmt.Sprintf("CRITICAL - Got response HTTP/1.1 %s, expected %s|%s", strconv.Itoa(res.StatusCode), strings.Join(expectedStatusCodes, ", "), timeInfo()), EXIT_CRITICAL, nil } // Check body text if len(e.BodyText) > 0 { expectedText := []byte(e.BodyText) bodyBytes, err := ioutil.ReadAll(res.Body) if err != nil { return "UNKNOWN", EXIT_UNKNOWN, err } if !bytes.Contains(bodyBytes, expectedText) { return fmt.Sprintf("CRITICAL - String '%s' not found in body|%s", e.BodyText, timeInfo()), EXIT_CRITICAL, nil } } // Check SSL cert if e.SSLCheck.Run { SSLMsg, SSLExit := checkCerts(res.TLS.VerifiedChains, e) if SSLExit != EXIT_OK { return fmt.Sprintf("%s|%s", SSLMsg, timeInfo()), SSLExit, nil } } return fmt.Sprintf("OK - Got response HTTP/1.1 %s|%s", strconv.Itoa(res.StatusCode), timeInfo()), EXIT_OK, nil } // Detects auth type func DetectAuthType(r *Request) int { client, err := initHTTPClient(r) if err != nil { // `Check` should handle all errors return AUTH_NONE } url := r.GetURL() request, err := http.NewRequest("GET", url, nil) if err != nil { // `Check` should handle all errors return AUTH_NONE } // User agent setUserAgent(request) // SNI if !r.NoSNI && len(r.Host) > 0 { request.Host = r.Host } res, err := client.Do(request) if err != nil { // `Check` should handle all errors return AUTH_NONE } defer res.Body.Close() authHeaders, ok := res.Header["Www-Authenticate"] if ok { authHeader := strings.ToLower(authHeaders[0]) if strings.HasPrefix(authHeader, "ntlm") || strings.HasPrefix(authHeader, "negotiate") { return AUTH_NTLM } if strings.HasPrefix(authHeader, "basic") { return AUTH_BASIC } } return AUTH_NONE }