Skip to content

Commit 32f6b57

Browse files
committed
Merge branch 'main' into event-duration-windows-zero
2 parents 92534c2 + 9382cc2 commit 32f6b57

File tree

8 files changed

+190
-15
lines changed

8 files changed

+190
-15
lines changed

CHANGELOG.next.asciidoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,8 @@ otherwise no tag is added. {issue}42208[42208] {pull}42403[42403]
428428
- Added support for websocket keep_alive heartbeat in the streaming input. {issue}42277[42277] {pull}44204[44204]
429429
- Add milliseconds to document timestamp from awscloudwatch Filebeat input {pull}44306[44306]
430430
- Add support to the Active Directory entity analytics provider for device entities. {pull}44309[44309]
431+
- Add support for OPTIONS request to HTTP Endpoint input. {issue}43930[43930] {pull}44387[44387]
432+
431433

432434
*Auditbeat*
433435

docs/reference/filebeat/filebeat-input-http_endpoint.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ These are the possible response codes from the server.
2020
| HTTP Response Code | Name | Reason |
2121
| --- | --- | --- |
2222
| 200 | OK | Returned on success. |
23-
| 400 | Bad Request | Returned if JSON body decoding fails or if `wait_for_completion_timeout` query validation fails. |
23+
| 400 | Bad Request | Returned if JSON body decoding fails, if an OPTIONS request is made and `options_headers` has not been set in the config, or if `wait_for_completion_timeout` query validation fails. |
2424
| 401 | Unauthorized | Returned when basic auth, secret header, or HMAC validation fails. |
2525
| 405 | Method Not Allowed | Returned if methods other than POST are used. |
2626
| 406 | Not Acceptable | Returned if the POST request does not contain a body. |
@@ -57,6 +57,21 @@ filebeat.inputs:
5757
prefix: "json"
5858
```
5959
60+
OPTIONS request-aware example:
61+
62+
```yaml
63+
filebeat.inputs:
64+
- type: http_endpoint
65+
enabled: true
66+
listen_address: 192.168.1.1
67+
listen_port: 8080
68+
response_code: 200
69+
options_headers:
70+
Custom-Options-Header: [custom-options-header-value]
71+
url: "/"
72+
prefix: "json"
73+
```
74+
6075
Map request to root of document example:
6176
6277
```yaml
@@ -285,6 +300,16 @@ The HTTP response code returned upon success. Should be in the 2XX range.
285300
The response body returned upon success.
286301

287302

303+
### `options_headers` [_options_headers]
304+
305+
A set of response headers to add to the response for OPTIONS requests. Headers with the same canonical MIME header name will be replaced with the values in this configuration.
306+
307+
308+
### `options_response_code` [_options_response_code]
309+
310+
The HTTP response code to return for OPTIONS requests. Defaults to `200` (OK).
311+
312+
288313
### `listen_address` [_listen_address]
289314

290315
If multiple interfaces is present the `listen_address` can be set to control which IP address the listener binds to. Defaults to `127.0.0.1`.

x-pack/filebeat/input/http_endpoint/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ type config struct {
3232
Password string `config:"password"`
3333
ResponseCode int `config:"response_code" validate:"positive"`
3434
ResponseBody string `config:"response_body"`
35+
OptionsHeaders http.Header `config:"options_headers"`
36+
OptionsStatus int `config:"options_response_code"`
3537
ListenAddress string `config:"listen_address"`
3638
ListenPort string `config:"listen_port"`
3739
URL string `config:"url" validate:"required"`
@@ -69,6 +71,7 @@ func defaultConfig() config {
6971
BasicAuth: false,
7072
ResponseCode: 200,
7173
ResponseBody: `{"message": "success"}`,
74+
OptionsStatus: 200,
7275
RetryAfter: 10,
7376
ListenAddress: "127.0.0.1",
7477
ListenPort: "8000",

x-pack/filebeat/input/http_endpoint/handler.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"io"
1414
"net"
1515
"net/http"
16+
"net/textproto"
1617
"net/url"
1718
"reflect"
1819
"sort"
@@ -84,13 +85,21 @@ type handler struct {
8485

8586
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
8687
txID := h.nextTxID()
87-
h.log.Debugw("request", "url", r.URL, "tx_id", txID)
88+
h.log.Debugw("request", "url", r.URL, "method", r.Method, "tx_id", txID)
8889
status, err := h.validator.validateRequest(r)
8990
if err != nil {
9091
h.sendAPIErrorResponse(txID, w, r, h.log, status, err)
9192
return
9293
}
9394

95+
if r.Method == http.MethodOptions {
96+
for k, v := range h.validator.optionsHeaders {
97+
w.Header()[textproto.CanonicalMIMEHeaderKey(k)] = v
98+
}
99+
w.WriteHeader(h.validator.optionsStatus)
100+
return
101+
}
102+
94103
wait, err := getTimeoutWait(r.URL, h.log)
95104
if err != nil {
96105
h.sendAPIErrorResponse(txID, w, r, h.log, http.StatusBadRequest, err)

x-pack/filebeat/input/http_endpoint/handler_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,52 @@ func Test_apiResponse(t *testing.T) {
257257
wantStatus: http.StatusOK,
258258
wantResponse: `{"message": "success"}`,
259259
},
260+
{
261+
name: "options_with_headers",
262+
conf: func() config {
263+
c := defaultConfig()
264+
c.OptionsHeaders = http.Header{
265+
"optional-response-header": {"Optional-response-value"},
266+
}
267+
return c
268+
}(),
269+
request: func() *http.Request {
270+
req := httptest.NewRequest(http.MethodOptions, "/", nil)
271+
req.Header.Set("Content-Type", "application/json")
272+
return req
273+
}(),
274+
events: []mapstr.M{},
275+
wantStatus: http.StatusOK,
276+
wantResponse: "",
277+
},
278+
{
279+
name: "options_empty_headers",
280+
conf: func() config {
281+
c := defaultConfig()
282+
c.OptionsHeaders = http.Header{}
283+
return c
284+
}(),
285+
request: func() *http.Request {
286+
req := httptest.NewRequest(http.MethodOptions, "/", nil)
287+
req.Header.Set("Content-Type", "application/json")
288+
return req
289+
}(),
290+
events: []mapstr.M{},
291+
wantStatus: http.StatusOK,
292+
wantResponse: "",
293+
},
294+
{
295+
name: "options_no_header",
296+
conf: defaultConfig(),
297+
request: func() *http.Request {
298+
req := httptest.NewRequest(http.MethodOptions, "/", nil)
299+
req.Header.Set("Content-Type", "application/json")
300+
return req
301+
}(),
302+
events: []mapstr.M{},
303+
wantStatus: http.StatusBadRequest,
304+
wantResponse: `{"message":"OPTIONS requests are only allowed with options_headers set"}`,
305+
},
260306
{
261307
name: "hmac_hex",
262308
setup: func(t *testing.T) { testutils.SkipIfFIPSOnly(t, "test HMAC uses SHA-1.") },

x-pack/filebeat/input/http_endpoint/input.go

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -335,18 +335,20 @@ func newHandler(ctx context.Context, c config, prg *program, pub func(beat.Event
335335
publish: pub,
336336
metrics: metrics,
337337
validator: apiValidator{
338-
basicAuth: c.BasicAuth,
339-
username: c.Username,
340-
password: c.Password,
341-
method: c.Method,
342-
contentType: c.ContentType,
343-
secretHeader: c.SecretHeader,
344-
secretValue: c.SecretValue,
345-
hmacHeader: c.HMACHeader,
346-
hmacKey: c.HMACKey,
347-
hmacType: c.HMACType,
348-
hmacPrefix: c.HMACPrefix,
349-
maxBodySize: -1,
338+
basicAuth: c.BasicAuth,
339+
username: c.Username,
340+
password: c.Password,
341+
method: c.Method,
342+
contentType: c.ContentType,
343+
secretHeader: c.SecretHeader,
344+
secretValue: c.SecretValue,
345+
hmacHeader: c.HMACHeader,
346+
hmacKey: c.HMACKey,
347+
hmacType: c.HMACType,
348+
hmacPrefix: c.HMACPrefix,
349+
maxBodySize: -1,
350+
optionsHeaders: c.OptionsHeaders,
351+
optionsStatus: c.OptionsStatus,
350352
},
351353
maxInFlight: c.MaxInFlight,
352354
retryAfter: c.RetryAfter,

x-pack/filebeat/input/http_endpoint/input_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,81 @@ var serverPoolTests = []struct {
116116
{"json": mapstr.M{"c": int64(3)}},
117117
},
118118
},
119+
{
120+
name: "options_with_headers",
121+
method: http.MethodOptions,
122+
cfgs: []*httpEndpoint{{
123+
addr: "127.0.0.1:9001",
124+
config: config{
125+
ResponseCode: http.StatusOK,
126+
ResponseBody: `{"message": "success"}`,
127+
OptionsStatus: http.StatusOK,
128+
OptionsHeaders: http.Header{"option-header": {"options-header-value"}},
129+
ListenAddress: "127.0.0.1",
130+
ListenPort: "9001",
131+
URL: "/",
132+
Prefix: "json",
133+
ContentType: "application/json",
134+
},
135+
}},
136+
events: []target{
137+
{
138+
url: "http://127.0.0.1:9001/", wantHeader: http.Header{
139+
"Content-Length": {"0"},
140+
"Option-Header": {"options-header-value"},
141+
},
142+
},
143+
},
144+
wantStatus: http.StatusOK,
145+
},
146+
{
147+
name: "options_empty_headers",
148+
method: http.MethodOptions,
149+
cfgs: []*httpEndpoint{{
150+
addr: "127.0.0.1:9001",
151+
config: config{
152+
ResponseCode: http.StatusOK,
153+
ResponseBody: `{"message": "success"}`,
154+
OptionsStatus: http.StatusOK,
155+
OptionsHeaders: http.Header{},
156+
ListenAddress: "127.0.0.1",
157+
ListenPort: "9001",
158+
URL: "/",
159+
Prefix: "json",
160+
ContentType: "application/json",
161+
},
162+
}},
163+
events: []target{
164+
{
165+
url: "http://127.0.0.1:9001/", wantHeader: http.Header{
166+
"Content-Length": {"0"},
167+
},
168+
},
169+
},
170+
wantStatus: http.StatusOK,
171+
},
172+
{
173+
name: "options_no_headers",
174+
method: http.MethodOptions,
175+
cfgs: []*httpEndpoint{{
176+
addr: "127.0.0.1:9001",
177+
config: config{
178+
ResponseCode: http.StatusOK,
179+
ResponseBody: `{"message": "success"}`,
180+
OptionsStatus: http.StatusOK,
181+
OptionsHeaders: nil,
182+
ListenAddress: "127.0.0.1",
183+
ListenPort: "9001",
184+
URL: "/",
185+
Prefix: "json",
186+
ContentType: "application/json",
187+
},
188+
}},
189+
events: []target{
190+
{url: "http://127.0.0.1:9001/", wantBody: `{"message":"OPTIONS requests are only allowed with options_headers set"}` + "\n"},
191+
},
192+
wantStatus: http.StatusBadRequest,
193+
},
119194
{
120195
name: "distinct_ports",
121196
cfgs: []*httpEndpoint{

x-pack/filebeat/input/http_endpoint/validate.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ type apiValidator struct {
3838
hmacType string
3939
hmacPrefix string
4040
maxBodySize int64
41+
42+
optionsHeaders http.Header
43+
optionsStatus int
4144
}
4245

4346
func (v *apiValidator) validateRequest(r *http.Request) (status int, err error) {
@@ -54,7 +57,10 @@ func (v *apiValidator) validateRequest(r *http.Request) (status int, err error)
5457
}
5558
}
5659

57-
if v.method != "" && v.method != r.Method {
60+
if !v.isMethodOK(r.Method) {
61+
if r.Method == http.MethodOptions {
62+
return http.StatusBadRequest, errors.New("OPTIONS requests are only allowed with options_headers set")
63+
}
5864
return http.StatusMethodNotAllowed, fmt.Errorf("only %v requests are allowed", v.method)
5965
}
6066

@@ -109,6 +115,13 @@ func (v *apiValidator) validateRequest(r *http.Request) (status int, err error)
109115
return http.StatusAccepted, nil
110116
}
111117

118+
func (v *apiValidator) isMethodOK(m string) bool {
119+
if m == http.MethodOptions {
120+
return v.optionsHeaders != nil
121+
}
122+
return v.method == "" || m == v.method
123+
}
124+
112125
// decoders is the priority-ordered set of decoders to use for HMAC header values.
113126
var decoders = [...]func(string) ([]byte, error){
114127
hex.DecodeString,

0 commit comments

Comments
 (0)