github.com/grafana/synthetic-monitoring-agent@v0.64.2/internal/prober/http/testserver/testserver.go (about) 1 package testserver 2 3 import ( 4 "encoding/base64" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io" 9 "log" 10 "net/http" 11 "net/http/httptest" 12 "net/url" 13 "strconv" 14 "strings" 15 ) 16 17 const ( 18 MaxBodySize = 1024 19 MaxHeaderValueLen = 256 20 ) 21 22 var allowedHeaders = []string{ 23 "age", 24 "allow", 25 "date", 26 "content-encoding", 27 "content-type", 28 "expires", 29 "last-modified", 30 "location", 31 } 32 33 type Settings struct { 34 Scheme string 35 Method string 36 Status int 37 Headers map[string]string 38 Body []byte 39 } 40 41 func (s Settings) URL(addr string) string { 42 scheme := s.Scheme 43 if len(scheme) == 0 { 44 scheme = "http" 45 } 46 47 u := url.URL{ 48 Scheme: scheme, 49 Host: addr, 50 Path: "/", 51 } 52 53 v := make(url.Values) 54 55 if len(s.Method) == 0 { 56 v.Set("method", http.MethodGet) 57 } else { 58 v.Set("method", s.Method) 59 } 60 61 v.Set("status", strconv.Itoa(s.Status)) 62 63 for name, value := range s.Headers { 64 v.Add("headers", fmt.Sprintf("%s:%s", name, value)) 65 } 66 67 if len(s.Body) > 0 { 68 v.Add("body", base64.StdEncoding.EncodeToString(s.Body)) 69 } 70 71 u.RawQuery = v.Encode() 72 73 return u.String() 74 } 75 76 type Config struct { 77 CertFn string 78 KeyFn string 79 AllowedDomains string 80 } 81 82 func New(cfg Config) *httptest.Server { 83 h := httpHandler{ 84 allowedDomains: makeSet(strings.Split(cfg.AllowedDomains, ",")), 85 allowedHeaders: makeSet(allowedHeaders), 86 } 87 88 mux := http.NewServeMux() 89 mux.HandleFunc("/", h.generator) 90 91 return httptest.NewServer(mux) 92 } 93 94 type httpHandler struct { 95 allowedDomains set 96 allowedHeaders set 97 } 98 99 func (h *httpHandler) generator(w http.ResponseWriter, req *http.Request) { 100 res := struct { 101 StatusCode int `json:"status"` 102 Message string `json:"message"` 103 Error string `json:"error,omitempty"` 104 Body io.Reader `json:"-"` 105 }{ 106 StatusCode: http.StatusOK, 107 } 108 109 w.Header().Set("Alt-Svc", "clear") 110 w.Header().Set("Cache-Control", "no-cache") 111 w.Header().Set("Content-Security-Policy", "default-src 'none'") 112 w.Header().Set("Cross-Origin-Embedder-Policy", "unsafe-none") 113 w.Header().Set("Cross-Origin-Opener-Policy", "unsafe-none") 114 w.Header().Set("Cross-Origin-Resource-Policy", "same-site") 115 w.Header().Set("X-Content-Type-Options", "nosniff") 116 w.Header().Set("X-Frame-Options", "deny") 117 118 defer func() { 119 if res.Message != "" || res.Error != "" { 120 w.Header().Set("Content-Type", "application/json") 121 w.WriteHeader(res.StatusCode) 122 123 enc := json.NewEncoder(w) 124 125 err := enc.Encode(&res) 126 if err != nil { 127 log.Printf("E: encoding response: %s", err) 128 } 129 130 return 131 } 132 133 w.WriteHeader(res.StatusCode) 134 135 if res.Body != nil { 136 _, _ = io.Copy(w, res.Body) 137 } 138 }() 139 140 if err := req.ParseForm(); err != nil { 141 res.StatusCode = http.StatusBadRequest 142 res.Message = "cannot parse input" 143 res.Error = err.Error() 144 145 return 146 } 147 148 if err := processMethod(req.Form["method"], req.Method); err != nil { 149 res.StatusCode = http.StatusBadRequest 150 res.Error = "invalid method" 151 res.Message = err.Error() 152 153 return 154 } 155 156 if err := processHeaders(req.Form["headers"], w, h.allowedDomains, h.allowedHeaders); err != nil { 157 res.StatusCode = http.StatusBadRequest 158 res.Message = "cannot process request" 159 res.Error = err.Error() 160 161 return 162 } 163 164 if val, found := req.Form["status"]; found { 165 res.StatusCode = processStatus(val) 166 if res.StatusCode/100 == 3 && h.allowedDomains.IsEmpty() { 167 res.StatusCode = http.StatusBadRequest 168 res.Error = "invalid status code" 169 res.Message = res.Error 170 171 return 172 } 173 } 174 175 if val, found := req.Form["body"]; found { 176 res.Body = io.LimitReader(base64.NewDecoder(base64.RawStdEncoding, strings.NewReader(val[0])), MaxBodySize) 177 } 178 } 179 180 func processMethod(method []string, actual string) error { 181 switch len(method) { 182 case 0: 183 return nil 184 185 case 1: 186 if method[0] != actual { 187 return fmt.Errorf("invalid method, expected %q, actual %q", method[0], actual) 188 } 189 190 default: 191 return fmt.Errorf("invalid expected method: %q", method) 192 } 193 194 return nil 195 } 196 197 func processHeaders(headers []string, w http.ResponseWriter, allowedDomains, allowedHeaders set) error { 198 for _, h := range headers { 199 key, val := splitHeader(h) 200 lcKey := strings.ToLower(key) 201 202 // Be overly strict: no CRs nor LFs allowed in header 203 // values at all, not just the CRLF sequence. 204 if strings.ContainsAny(val, "\r\n") { 205 return fmt.Errorf("invalid header value %q", val) 206 } 207 208 if allowedHeaders.Has(lcKey) { 209 return fmt.Errorf("header %q not allowed", key) 210 } 211 212 if len(val) > MaxHeaderValueLen { 213 return fmt.Errorf("header %q value too long", key) 214 } 215 216 if lcKey == "location" { 217 err := sanitizeLocation(val, allowedDomains) 218 if err != nil { 219 return fmt.Errorf("redirect to %q not allowed", val) 220 } 221 } 222 223 w.Header().Set(key, val) 224 } 225 226 return nil 227 } 228 229 func processStatus(status []string) int { 230 switch len(status) { 231 case 0: 232 return http.StatusBadRequest 233 234 case 1: 235 code, err := strconv.ParseInt(status[0], 10, 16) 236 if err != nil { 237 return http.StatusBadRequest 238 } 239 240 return int(code) 241 } 242 243 return http.StatusOK 244 } 245 246 func splitHeader(header string) (string, string) { 247 values := strings.SplitN(header, ":", 2) 248 249 var key, val string 250 251 switch len(values) { 252 case 2: 253 val = strings.TrimSpace(values[1]) 254 fallthrough 255 case 1: 256 key = strings.TrimSpace(values[0]) 257 } 258 259 return key, val 260 } 261 262 func sanitizeLocation(loc string, allowedDomains set) error { 263 if allowedDomains.IsEmpty() { 264 return errors.New("redirects not allowed") 265 } 266 267 target, err := url.Parse(loc) 268 if err != nil { 269 log.Printf("E: bad location %q", loc) 270 return err 271 } 272 273 found := false 274 275 for domain := range allowedDomains { 276 host := strings.ToLower(target.Host) 277 278 if !strings.HasSuffix(host, domain) { 279 continue 280 } 281 282 if host == domain || strings.HasSuffix(host, "."+domain) { 283 found = true 284 break 285 } 286 } 287 288 if !found { 289 return fmt.Errorf("redirect to %s not allowed", target.Host) 290 } 291 292 return nil 293 } 294 295 type set map[string]struct{} 296 297 func makeSet(headers []string) set { 298 out := make(map[string]struct{}) 299 300 for _, header := range headers { 301 if len(header) > 0 { 302 out[header] = struct{}{} 303 } 304 } 305 306 return out 307 } 308 309 func (s set) IsEmpty() bool { 310 return len(s) == 0 311 } 312 313 func (s set) Has(key string) bool { 314 _, found := s[key] 315 return found 316 }