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 }