Add support for HTTP request body as file (#987)
authorDaniel Teunis <daniel@teunis.cc>
Wed, 29 Mar 2023 16:50:09 +0000 (18:50 +0200)
committerGitHub <noreply@github.com>
Wed, 29 Mar 2023 16:50:09 +0000 (22:20 +0530)
* Add HTTP request body test

This commit adds a test for specifying a request body for HTTP probes.

Signed-off-by: Daniel Teunis <daniel@teunis.cc>
* Add support for HTTP request body as file

Resolves #391

Signed-off-by: Daniel Teunis <daniel@teunis.cc>
* Use io instead of io/ioutil

Signed-off-by: Marcelo Magallon <marcelo.magallon@gmail.com>
---------

Signed-off-by: Daniel Teunis <daniel@teunis.cc>
Signed-off-by: Marcelo Magallon <marcelo.magallon@gmail.com>
Co-authored-by: Marcelo Magallon <marcelo.magallon@gmail.com>
CONFIGURATION.md
config/config.go
config/config_test.go
config/testdata/invalid-http-body-config.yml [new file with mode: 0644]
example.yml
prober/http.go
prober/http_test.go

index a3347c3ef3b2592f28ee0b7f4277eb23c2d5b64f..604ee890d098f7b169603ddca81e58c70fd05e87 100644 (file)
@@ -142,7 +142,11 @@ modules:
   [ ip_protocol_fallback: <boolean> | default = true ]
 
   # The body of the HTTP request used in probe.
-  body: [ <string> ]
+  [ body: <string> ]
+
+  # Read the HTTP request body from from a file.
+  # It is mutually exclusive with `body`.
+  [ body_file: <filename> ]
 
 ```
 
index 246e52d804053be24d7d8bef693a0150d080c84e..99338843f56ea4c3bf85c10ba2c3791a64578653 100644 (file)
@@ -218,6 +218,7 @@ type HTTPProbe struct {
        FailIfHeaderMatchesRegexp    []HeaderMatch           `yaml:"fail_if_header_matches,omitempty"`
        FailIfHeaderNotMatchesRegexp []HeaderMatch           `yaml:"fail_if_header_not_matches,omitempty"`
        Body                         string                  `yaml:"body,omitempty"`
+       BodyFile                     string                  `yaml:"body_file,omitempty"`
        HTTPClientConfig             config.HTTPClientConfig `yaml:"http_client_config,inline"`
        Compression                  string                  `yaml:"compression,omitempty"`
        BodySizeLimit                units.Base2Bytes        `yaml:"body_size_limit,omitempty"`
@@ -330,6 +331,10 @@ func (s *HTTPProbe) UnmarshalYAML(unmarshal func(interface{}) error) error {
                s.HTTPClientConfig.FollowRedirects = !*s.NoFollowRedirects
        }
 
+       if s.Body != "" && s.BodyFile != "" {
+               return errors.New("setting body and body_file both are not allowed")
+       }
+
        for key, value := range s.Headers {
                switch textproto.CanonicalMIMEHeaderKey(key) {
                case "Accept-Encoding":
index e1b57a3e0314b202ec22567d8d1459af5b7ee70c..65e3fe8481a5603a770d4bc9378dd5a3516dd28d 100644 (file)
@@ -99,6 +99,10 @@ func TestLoadBadConfigs(t *testing.T) {
                        input: "testdata/invalid-tcp-query-response-regexp.yml",
                        want:  `error parsing config file: "Could not compile regular expression" regexp=":["`,
                },
+               {
+                       input: "testdata/invalid-http-body-config.yml",
+                       want:  `error parsing config file: setting body and body_file both are not allowed`,
+               },
        }
        for _, test := range tests {
                t.Run(test.input, func(t *testing.T) {
diff --git a/config/testdata/invalid-http-body-config.yml b/config/testdata/invalid-http-body-config.yml
new file mode 100644 (file)
index 0000000..16c4784
--- /dev/null
@@ -0,0 +1,7 @@
+modules:
+  http_test:
+    prober: http
+    timeout: 5s
+    http:
+      body: "Test body"
+      body_file: "test_body.txt"
index b7ac278315297087400c22cb3893d42c6e40a418..5e887a54d3fa4ed3adf23bd0fb39f9063ae44e5e 100644 (file)
@@ -48,6 +48,12 @@ modules:
       headers:
         Content-Type: application/json
       body: '{}'
+  http_post_body_file:
+    prober: http
+    timeout: 5s
+    http:
+      method: POST
+      body_file: "/files/body.txt"
   http_basic_auth_example:
     prober: http
     timeout: 5s
index af12c7e427985ac0239ebd011326fc48020aeeec..5e452cd94313367bf7fffdbfb65fa906a2a40306 100644 (file)
@@ -27,6 +27,7 @@ import (
        "net/http/httptrace"
        "net/textproto"
        "net/url"
+       "os"
        "strconv"
        "strings"
        "sync"
@@ -408,6 +409,17 @@ func ProbeHTTP(ctx context.Context, target string, module config.Module, registr
                body = strings.NewReader(httpConfig.Body)
        }
 
+       // If a body file is configured, add its content to the request.
+       if httpConfig.BodyFile != "" {
+               body_file, err := os.Open(httpConfig.BodyFile)
+               if err != nil {
+                       level.Error(logger).Log("msg", "Error creating request", "err", err)
+                       return
+               }
+               defer body_file.Close()
+               body = body_file
+       }
+
        request, err := http.NewRequest(httpConfig.Method, targetURL.String(), body)
        if err != nil {
                level.Error(logger).Log("msg", "Error creating request", "err", err)
index 69fcceefc06c7a17071404c7dcbb999b9d603ec6..3d7dfdb21751155de8b887ef2177dd93d36b826a 100644 (file)
@@ -22,6 +22,7 @@ import (
        "crypto/x509"
        "encoding/pem"
        "fmt"
+       "io"
        "net/http"
        "net/http/httptest"
        "net/textproto"
@@ -1405,3 +1406,51 @@ func TestSkipResolvePhase(t *testing.T) {
                checkMetrics(expectedMetrics, mfs, t)
        })
 }
+
+func TestBody(t *testing.T) {
+       body := "Test Body"
+       tmpBodyFile, err := os.CreateTemp("", "body.txt")
+       if err != nil {
+               t.Fatalf("Error creating body tempfile: %s", err)
+       }
+       if _, err := tmpBodyFile.Write([]byte(body)); err != nil {
+               t.Fatalf("Error writing body tempfile: %s", err)
+       }
+       if err := tmpBodyFile.Close(); err != nil {
+               t.Fatalf("Error closing body tempfie: %s", err)
+       }
+
+       tests := []config.HTTPProbe{
+               {IPProtocolFallback: true, Body: body},
+               {IPProtocolFallback: true, BodyFile: tmpBodyFile.Name()},
+       }
+
+       for i, test := range tests {
+               ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+                       b, err := io.ReadAll(r.Body)
+                       if err != nil {
+                               t.Fatalf("Body test %d failed unexpectedly.", i)
+                       }
+                       if string(b) != body {
+                               t.Fatalf("Body test %d failed unexpectedly.", i)
+                       }
+               }))
+               defer ts.Close()
+
+               registry := prometheus.NewRegistry()
+               testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+               defer cancel()
+               result := ProbeHTTP(
+                       testCTX,
+                       ts.URL,
+                       config.Module{
+                               Timeout: time.Second,
+                               HTTP:    test},
+                       registry,
+                       log.NewNopLogger(),
+               )
+               if !result {
+                       t.Fatalf("Body test %d failed unexpectedly.", i)
+               }
+       }
+}