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  }