github.com/google/cloudprober@v0.11.3/validators/http/http.go (about)

     1  // Copyright 2018 The Cloudprober Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package http provides an HTTP validator for the Cloudprober's validator
    16  // framework.
    17  package http
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	nethttp "net/http"
    23  	"regexp"
    24  	"strconv"
    25  	"strings"
    26  
    27  	"github.com/google/cloudprober/logger"
    28  	configpb "github.com/google/cloudprober/validators/http/proto"
    29  )
    30  
    31  // Validator implements a validator for HTTP responses.
    32  type Validator struct {
    33  	c *configpb.Validator
    34  	l *logger.Logger
    35  
    36  	successStatusCodeRanges []*numRange
    37  	failureStatusCodeRanges []*numRange
    38  	successHeaderRegexp     *regexp.Regexp
    39  	failureHeaderRegexp     *regexp.Regexp
    40  }
    41  
    42  type numRange struct {
    43  	lower int
    44  	upper int
    45  }
    46  
    47  func (nr *numRange) find(i int) bool {
    48  	return i >= nr.lower && i <= nr.upper
    49  }
    50  
    51  // parseNumRange parses number range from the given string:
    52  // for example:
    53  //          200-299: &numRange{200, 299}
    54  //          403:     &numRange{403, 403}
    55  func parseNumRange(s string) (*numRange, error) {
    56  	fields := strings.Split(s, "-")
    57  	if len(fields) < 1 || len(fields) > 2 {
    58  		return nil, fmt.Errorf("number range %s is not in correct format (200 or 100-199)", s)
    59  	}
    60  
    61  	lower, err := strconv.ParseInt(fields[0], 10, 32)
    62  	if err != nil {
    63  		return nil, fmt.Errorf("got error while parsing the range's lower bound (%s): %v", fields[0], err)
    64  	}
    65  
    66  	// If there is only one number, set upper = lower.
    67  	if len(fields) == 1 {
    68  		return &numRange{int(lower), int(lower)}, nil
    69  	}
    70  
    71  	upper, err := strconv.ParseInt(fields[1], 10, 32)
    72  	if err != nil {
    73  		return nil, fmt.Errorf("got error while parsing the range's upper bound (%s): %v", fields[1], err)
    74  	}
    75  
    76  	if upper < lower {
    77  		return nil, fmt.Errorf("upper bound cannot be smaller than the lower bound (%s)", s)
    78  	}
    79  
    80  	return &numRange{int(lower), int(upper)}, nil
    81  }
    82  
    83  // parseStatusCodeConfig parses the status code config. Status codes are
    84  // defined as a comma-separated list of integer or integer ranges, for
    85  // example: 302,200-299.
    86  func parseStatusCodeConfig(s string) ([]*numRange, error) {
    87  	var statusCodeRanges []*numRange
    88  
    89  	for _, codeStr := range strings.Split(s, ",") {
    90  		nr, err := parseNumRange(codeStr)
    91  		if err != nil {
    92  			return nil, err
    93  		}
    94  		statusCodeRanges = append(statusCodeRanges, nr)
    95  	}
    96  	return statusCodeRanges, nil
    97  }
    98  
    99  // lookupStatusCode looks up a given status code in status code map and status
   100  // code ranges.
   101  func lookupStatusCode(statusCode int, statusCodeRanges []*numRange) bool {
   102  	// Look for the statusCode in statusCodeRanges.
   103  	for _, cr := range statusCodeRanges {
   104  		if cr.find(statusCode) {
   105  			return true
   106  		}
   107  	}
   108  
   109  	return false
   110  }
   111  
   112  // lookupHTTPHeader looks up for the given header in the HTTP response. It
   113  // returns true on the first match. If valueRegex is omitted - check for header
   114  // existence only.
   115  func lookupHTTPHeader(headers nethttp.Header, expectedHeader string, valueRegexp *regexp.Regexp) bool {
   116  	values, found := headers[expectedHeader]
   117  	if !found {
   118  		return false
   119  	}
   120  
   121  	// Return true if not interested in header's value.
   122  	if valueRegexp == nil {
   123  		return true
   124  	}
   125  
   126  	for _, value := range values {
   127  		if valueRegexp.MatchString(value) {
   128  			return true
   129  		}
   130  	}
   131  
   132  	return false
   133  }
   134  
   135  func (v *Validator) initHeaderValidators(c *configpb.Validator) error {
   136  	parseHeader := func(h *configpb.Validator_Header) (*regexp.Regexp, error) {
   137  		if h == nil {
   138  			return nil, nil
   139  		}
   140  		if h.GetName() == "" {
   141  			return nil, errors.New("header name cannot be empty")
   142  		}
   143  		if h.GetValueRegex() == "" {
   144  			return nil, nil
   145  		}
   146  		return regexp.Compile(h.GetValueRegex())
   147  	}
   148  
   149  	var err error
   150  
   151  	if v.successHeaderRegexp, err = parseHeader(c.GetSuccessHeader()); err != nil {
   152  		return fmt.Errorf("invalid-success-header: %v", err)
   153  	}
   154  
   155  	if v.failureHeaderRegexp, err = parseHeader(c.GetFailureHeader()); err != nil {
   156  		return fmt.Errorf("invalid-failure-header: %v", err)
   157  	}
   158  
   159  	return nil
   160  }
   161  
   162  // Init initializes the HTTP validator.
   163  func (v *Validator) Init(config interface{}, l *logger.Logger) error {
   164  	c, ok := config.(*configpb.Validator)
   165  	if !ok {
   166  		return fmt.Errorf("%v is not a valid HTTP validator config", config)
   167  	}
   168  
   169  	v.c = c
   170  	v.l = l
   171  
   172  	var err error
   173  	if c.GetSuccessStatusCodes() != "" {
   174  		v.successStatusCodeRanges, err = parseStatusCodeConfig(c.GetSuccessStatusCodes())
   175  		if err != nil {
   176  			return err
   177  		}
   178  	}
   179  
   180  	if c.GetFailureStatusCodes() != "" {
   181  		v.failureStatusCodeRanges, err = parseStatusCodeConfig(c.GetFailureStatusCodes())
   182  		if err != nil {
   183  			return err
   184  		}
   185  	}
   186  
   187  	return v.initHeaderValidators(c)
   188  }
   189  
   190  // Validate the provided input and return true if input is valid. Validate
   191  // expects the input to be of the type: *http.Response. Note that it doesn't
   192  // use the string input, it's part of the function signature to satisfy
   193  // Validator interface.
   194  func (v *Validator) Validate(input interface{}, unused []byte) (bool, error) {
   195  	res, ok := input.(*nethttp.Response)
   196  	if !ok {
   197  		return false, fmt.Errorf("input %v is not of type http.Response", input)
   198  	}
   199  
   200  	if v.c.GetFailureStatusCodes() != "" {
   201  		if lookupStatusCode(res.StatusCode, v.failureStatusCodeRanges) {
   202  			return false, nil
   203  		}
   204  	}
   205  
   206  	if failureHeader := v.c.GetFailureHeader(); failureHeader != nil {
   207  		if lookupHTTPHeader(res.Header, failureHeader.GetName(), v.failureHeaderRegexp) {
   208  			return false, nil
   209  		}
   210  	}
   211  
   212  	if v.c.GetSuccessStatusCodes() != "" {
   213  		if !lookupStatusCode(res.StatusCode, v.successStatusCodeRanges) {
   214  			return false, nil
   215  		}
   216  	}
   217  
   218  	if successHeader := v.c.GetSuccessHeader(); successHeader != nil {
   219  		if !lookupHTTPHeader(res.Header, successHeader.GetName(), v.successHeaderRegexp) {
   220  			return false, nil
   221  		}
   222  	}
   223  
   224  	return true, nil
   225  }