Skip to content

Commit

Permalink
x-pack/filebeat/input/cel: add default user-agent to http requests
Browse files Browse the repository at this point in the history
  • Loading branch information
efd6 committed May 15, 2024
1 parent 0ac96f4 commit b6221d3
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 4 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.next.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ https://github.com/elastic/beats/compare/v8.8.1\...main[Check the HEAD diff]
- Update CEL mito extensions to v1.11.0 to improve type checking. {pull}39460[39460]
- Improve logging of request and response with request trace logging in error conditions. {pull}39455[39455]
- Add HTTP metrics to CEL input. {issue}39501[39501] {pull}39503[39503]
- Add default user-agent to CEL HTTP requests. {issue}39502[39502] {pull}[]

*Auditbeat*

Expand Down
2 changes: 1 addition & 1 deletion x-pack/filebeat/docs/inputs/input-cel.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ As noted above the `cel` input provides functions, macros, and global variables
* {mito_docs}/lib#Debug[Debug] — the debug handler registers a logger with the name extension `cel_debug` and calls to the CEL `debug` function are emitted to that logger.
** {mito_docs}/lib#hdr-Debug[Debug]

In addition to the extensions provided in the packages listed above, a global variable `useragent` is also provided which gives the user CEL program access to the {beatname_lc} user-agent string.
In addition to the extensions provided in the packages listed above, a global variable `useragent` is also provided which gives the user CEL program access to the {beatname_lc} user-agent string. By default, this value is assigned to all requests' user-agent headers unless the CEL program has already set the user-agent header value. Programs wishing to not provide a user-agent, should set this header to the empty string, `""`.

The CEL environment enables the https://pkg.go.dev/github.com/google/cel-go/cel#OptionalTypes[optional types] library using the version defined {mito_docs}/lib#OptionalTypesVersion[here].

Expand Down
20 changes: 19 additions & 1 deletion x-pack/filebeat/input/cel/input.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ const (
root = "state"
)

// The Filebeat user-agent is provided to the program as useragent.
// The Filebeat user-agent is provided to the program as useragent. If a request
// is not given a user-agent string, this user agent is added to the request.
var userAgent = useragent.UserAgent("Filebeat", version.GetDefaultVersion(), version.Commit(), version.BuildTime().String())

func Plugin(log *logp.Logger, store inputcursor.StateStore) v2.Plugin {
Expand Down Expand Up @@ -758,6 +759,11 @@ func newClient(ctx context.Context, cfg config, log *logp.Logger, reg *monitorin
return authClient, trace, nil
}

c.Transport = userAgentDecorator{
UserAgent: userAgent,
Transport: c.Transport,
}

return c, trace, nil
}

Expand Down Expand Up @@ -856,6 +862,18 @@ func retryErrorHandler(max int, log *logp.Logger) retryablehttp.ErrorHandler {
}
}

type userAgentDecorator struct {
UserAgent string
Transport http.RoundTripper
}

func (t userAgentDecorator) RoundTrip(r *http.Request) (*http.Response, error) {
if _, ok := r.Header["User-Agent"]; !ok {
r.Header.Set("User-Agent", t.UserAgent)
}
return t.Transport.RoundTrip(r)
}

func newRateLimiterFromConfig(cfg *ResourceConfig) *rate.Limiter {
r := rate.Inf
b := 1
Expand Down
140 changes: 138 additions & 2 deletions x-pack/filebeat/input/cel/input_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,142 @@ var inputTests = []struct {
},
},
},
{
name: "GET_request_check_user_agent_default",
server: newTestServer(httptest.NewServer),
config: map[string]interface{}{
"interval": 1,
"program": `
get(state.url).Body.as(body, {
"events": [body.decode_json()]
})
`,
},
handler: func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", "application/json")
msg := `{"hello":[{"world":"moon"},{"space":[{"cake":"pumpkin"}]}]}`
if r.UserAgent() != userAgent {
w.WriteHeader(http.StatusBadRequest)
msg = fmt.Sprintf(`{"error":"expected user agent was %#q"}`, userAgent)
}
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusBadRequest)
msg = fmt.Sprintf(`{"error":"expected method was %#q"}`, http.MethodGet)
}

w.Write([]byte(msg))
},
want: []map[string]interface{}{
{
"hello": []interface{}{
map[string]interface{}{
"world": "moon",
},
map[string]interface{}{
"space": []interface{}{
map[string]interface{}{
"cake": "pumpkin",
},
},
},
},
},
},
},
{
name: "GET_request_check_user_agent_user_defined",
server: newTestServer(httptest.NewServer),
config: map[string]interface{}{
"interval": 1,
"program": `
get_request(state.url).with({
"Header": {
"User-Agent": ["custom user agent"]
}
}).do_request().Body.as(body, {
"events": [body.decode_json()]
})
`,
},
handler: func(w http.ResponseWriter, r *http.Request) {
const customUserAgent = "custom user agent"

w.Header().Set("content-type", "application/json")
msg := `{"hello":[{"world":"moon"},{"space":[{"cake":"pumpkin"}]}]}`
if r.UserAgent() != customUserAgent {
w.WriteHeader(http.StatusBadRequest)
msg = fmt.Sprintf(`{"error":"expected user agent was %#q"}`, customUserAgent)
}
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusBadRequest)
msg = fmt.Sprintf(`{"error":"expected method was %#q"}`, http.MethodGet)
}

w.Write([]byte(msg))
},
want: []map[string]interface{}{
{
"hello": []interface{}{
map[string]interface{}{
"world": "moon",
},
map[string]interface{}{
"space": []interface{}{
map[string]interface{}{
"cake": "pumpkin",
},
},
},
},
},
},
},
{
name: "GET_request_check_user_agent_none",
server: newTestServer(httptest.NewServer),
config: map[string]interface{}{
"interval": 1,
"program": `
get_request(state.url).with({
"Header": {
"User-Agent": [""]
}
}).do_request().Body.as(body, {
"events": [body.decode_json()]
})
`,
},
handler: func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", "application/json")
msg := `{"hello":[{"world":"moon"},{"space":[{"cake":"pumpkin"}]}]}`
if _, ok := r.Header["User-Agent"]; ok {
w.WriteHeader(http.StatusBadRequest)
msg = fmt.Sprintf(`{"error":"expected no user agent header, but got %#q"}`, r.UserAgent())
}
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusBadRequest)
msg = fmt.Sprintf(`{"error":"expected method was %#q"}`, http.MethodGet)
}

w.Write([]byte(msg))
},
want: []map[string]interface{}{
{
"hello": []interface{}{
map[string]interface{}{
"world": "moon",
},
map[string]interface{}{
"space": []interface{}{
map[string]interface{}{
"cake": "pumpkin",
},
},
},
},
},
},
},
{
name: "GET_request_TLS",
server: newTestServer(httptest.NewTLSServer),
Expand Down Expand Up @@ -1576,13 +1712,13 @@ func defaultHandler(expectedMethod, expectedBody string) http.HandlerFunc {
switch {
case r.Method != expectedMethod:
w.WriteHeader(http.StatusBadRequest)
msg = fmt.Sprintf(`{"error":"expected method was %q"}`, expectedMethod)
msg = fmt.Sprintf(`{"error":"expected method was %#q"}`, expectedMethod)
case expectedBody != "":
body, _ := io.ReadAll(r.Body)
r.Body.Close()
if expectedBody != string(body) {
w.WriteHeader(http.StatusBadRequest)
msg = fmt.Sprintf(`{"error":"expected body was %q"}`, expectedBody)
msg = fmt.Sprintf(`{"error":"expected body was %#q"}`, expectedBody)
}
}

Expand Down

0 comments on commit b6221d3

Please sign in to comment.