Skip to content

Commit b237c7c

Browse files
[8.19](backport #6491) Fix checkin endpoint gzip handling (#6526)
* Fix checkin endpoint gzip handling (#6491) Detect Content-Encoding: gzip header when validating checkin bodies and uncompress the body if detected. (cherry picked from commit 122fac1) # Conflicts: # internal/pkg/api/handleCheckin_test.go * Fix backport * Fix backport test * Fix panic --------- Co-authored-by: Michel Laterman <82832767+michel-laterman@users.noreply.github.com> Co-authored-by: michel-laterman <michel.laterman@elastic.co>
1 parent 6d9c53c commit b237c7c

8 files changed

Lines changed: 176 additions & 24 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Kind can be one of:
2+
# - breaking-change: a change to previously-documented behavior
3+
# - deprecation: functionality that is being removed in a later release
4+
# - bug-fix: fixes a problem in a previous version
5+
# - enhancement: extends functionality but does not break or fix existing behavior
6+
# - feature: new functionality
7+
# - known-issue: problems that we are aware of in a given version
8+
# - security: impacts on the security of a product or a user’s deployment.
9+
# - upgrade: important information for someone upgrading from a prior version
10+
# - other: does not fit into any of the other categories
11+
kind: bug-fix
12+
13+
# Change summary; a 80ish characters long description of the change.
14+
summary: Fix checkin endpoint compression support
15+
16+
# Long description; in case the summary is not enough to describe the change
17+
# this field accommodate a description without length limits.
18+
# NOTE: This field will be rendered only for breaking-change and known-issue kinds at the moment.
19+
description: |
20+
Adds support for gzip compressed requests to the checkin endpoint.
21+
22+
# Affected component; usually one of "elastic-agent", "fleet-server", "filebeat", "metricbeat", "auditbeat", "all", etc.
23+
component: fleet-server
24+
25+
# PR URL; optional; the PR number that added the changeset.
26+
# If not present is automatically filled by the tooling finding the PR where this changelog fragment has been added.
27+
# NOTE: the tooling supports backports, so it's able to fill the original PR number instead of the backport PR number.
28+
# Please provide it if you are adding a fragment for a different PR.
29+
pr: https://github.com/elastic/fleet-server/pull/6491
30+
31+
# Issue URL; optional; the GitHub issue related to this changeset (either closes or is part of).
32+
# If not present is automatically filled by the tooling with the issue linked to the PR number.
33+
#issue: https://github.com/owner/repo/1234

internal/pkg/api/handleCheckin.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,20 @@ func (ct *CheckinT) validateRequest(zlog zerolog.Logger, w http.ResponseWriter,
181181
}
182182
readCounter := datacounter.NewReaderCounter(body)
183183

184+
// Decompress the body when the client signals Content-Encoding: gzip.
185+
var bodyReader io.Reader = readCounter
186+
if r.Header.Get("Content-Encoding") == kEncodingGzip {
187+
gr, err := gzip.NewReader(readCounter)
188+
if err != nil {
189+
return validatedCheckin{}, &BadRequestErr{msg: "unable to create gzip reader for request body", nextErr: err}
190+
}
191+
defer gr.Close()
192+
bodyReader = gr
193+
}
194+
184195
var val validatedCheckin
185196
var req CheckinRequest
186-
decoder := json.NewDecoder(readCounter)
197+
decoder := json.NewDecoder(bodyReader)
187198
if err := decoder.Decode(&req); err != nil {
188199
return val, &BadRequestErr{msg: "unable to decode checkin request", nextErr: err}
189200
}
@@ -643,6 +654,8 @@ func (ct *CheckinT) writeResponse(zlog zerolog.Logger, w http.ResponseWriter, r
643654
return err
644655
}
645656

657+
// acceptsEncoding reports whether the request includes the passed encoding.
658+
// Only an exact match is checked as that is all the agent will send.
646659
func acceptsEncoding(r *http.Request, encoding string) bool {
647660
for _, v := range r.Header.Values("Accept-Encoding") {
648661
if v == encoding {

internal/pkg/api/handleCheckin_test.go

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
package api
88

99
import (
10+
"bytes"
1011
"compress/flate"
12+
"compress/gzip"
1113
"context"
1214
"encoding/json"
1315
"errors"
@@ -1073,6 +1075,44 @@ func TestValidateCheckinRequest(t *testing.T) {
10731075
},
10741076
expValid: validatedCheckin{},
10751077
},
1078+
{
1079+
name: "gzip-compressed request body is decompressed before JSON decoding",
1080+
req: func() *http.Request {
1081+
var buf bytes.Buffer
1082+
gz := gzip.NewWriter(&buf)
1083+
_, _ = gz.Write([]byte(`{"status": "online", "message": "test message"}`))
1084+
_ = gz.Close()
1085+
return &http.Request{
1086+
Header: http.Header{"Content-Encoding": []string{"gzip"}},
1087+
Body: io.NopCloser(&buf),
1088+
}
1089+
}(),
1090+
cfg: &config.Server{
1091+
Limits: config.ServerLimits{
1092+
CheckinLimit: config.Limit{
1093+
MaxBody: 0,
1094+
},
1095+
},
1096+
},
1097+
expErr: nil,
1098+
expValid: validatedCheckin{},
1099+
},
1100+
{
1101+
name: "invalid gzip request body returns bad request error",
1102+
req: &http.Request{
1103+
Header: http.Header{"Content-Encoding": []string{"gzip"}},
1104+
Body: io.NopCloser(strings.NewReader(`not gzip data`)),
1105+
},
1106+
cfg: &config.Server{
1107+
Limits: config.ServerLimits{
1108+
CheckinLimit: config.Limit{
1109+
MaxBody: 0,
1110+
},
1111+
},
1112+
},
1113+
expErr: &BadRequestErr{msg: "unable to create gzip reader for request body", nextErr: gzip.ErrHeader},
1114+
expValid: validatedCheckin{},
1115+
},
10761116
}
10771117

10781118
for _, tc := range tests {
@@ -1081,7 +1121,7 @@ func TestValidateCheckinRequest(t *testing.T) {
10811121
assert.NoError(t, err)
10821122
wr := httptest.NewRecorder()
10831123
logger := testlog.SetLogger(t)
1084-
valid, err := checkin.validateRequest(logger, wr, tc.req, time.Time{}, nil)
1124+
valid, err := checkin.validateRequest(logger, wr, tc.req, time.Time{}, &model.Agent{LocalMetadata: json.RawMessage(`{}`)})
10851125
if tc.expErr == nil {
10861126
assert.NoError(t, err)
10871127
} else {
@@ -1090,8 +1130,8 @@ func TestValidateCheckinRequest(t *testing.T) {
10901130
// we will end up with false positives.
10911131
assert.Equal(t, tc.expErr.Error(), err.Error())
10921132
assert.ErrorAs(t, err, &tc.expErr)
1133+
assert.Equal(t, tc.expValid, valid)
10931134
}
1094-
assert.Equal(t, tc.expValid, valid)
10951135
})
10961136
}
10971137
}

internal/pkg/api/openapi.gen.go

Lines changed: 32 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/pkg/server/fleet_integration_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ const (
5050
serverVersion = "8.0.0"
5151
localhost = "localhost"
5252

53-
testWaitServerUp = 3 * time.Second
53+
testWaitServerUp = 10 * time.Second
5454

5555
enrollBody = `{
5656
"type": "PERMANENT",
@@ -242,6 +242,7 @@ func startTestServer(t *testing.T, ctx context.Context, policyD model.PolicyData
242242
}
243243

244244
func (s *tserver) waitServerUp(ctx context.Context, dur time.Duration) error {
245+
zlog := zerolog.Ctx(ctx)
245246
ctx, cancel := context.WithTimeout(ctx, dur)
246247
defer cancel()
247248

@@ -268,6 +269,7 @@ func (s *tserver) waitServerUp(ctx context.Context, dur time.Duration) error {
268269
return false, err
269270
}
270271

272+
zlog.Info().Msgf("test wait for fleet-server up status: %s", status.Status)
271273
return status.Status == "HEALTHY", nil
272274
}
273275

model/openapi.yml

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1372,11 +1372,18 @@ paths:
13721372
- name: Accept-Encoding
13731373
in: header
13741374
description: |
1375-
If the agent is able to accept encoded responses.
1376-
Used to indicate if GZIP compression may be used by the server.
1377-
The elastic-agent does not use the accept-encoding header.
1375+
Indicates encodings the agent can accept. The only encoding supported by the server currently is gzip.
1376+
Comma-separated directive lists and RFC 7231 quality values (e.g. "gzip;q=1.0, deflate;q=0.5") are both accepted.
13781377
schema:
13791378
type: string
1379+
- name: Content-Encoding
1380+
in: header
1381+
description: |
1382+
Signals that the request body has been compressed. Server currently only supports gzip compression.
1383+
schema:
1384+
type: string
1385+
enum:
1386+
- gzip
13801387
- $ref: "#/components/parameters/userAgent"
13811388
- $ref: "#/components/parameters/requestId"
13821389
- $ref: "#/components/parameters/apiVersion"
@@ -1412,12 +1419,19 @@ paths:
14121419
description: Agent checkin successful. May include actions.
14131420
headers:
14141421
Content-Encoding:
1415-
description: Responses may be compressed if the accept encoding indicates it. Currently not used by the agent.
1422+
description: |
1423+
Present when fleet-server has gzip-compressed the response body.
1424+
Compression is applied when all three conditions are met: the serialised
1425+
response exceeds the configured compression threshold, the server compression
1426+
level is not NoCompression, and the request's Accept-Encoding header
1427+
advertises gzip support.
14161428
schema:
14171429
type: string
1430+
enum:
1431+
- gzip
14181432
examples:
14191433
gzip:
1420-
description: Response is gzip encoded as the request headers allowed it.
1434+
description: Response body is gzip-compressed.
14211435
value: gzip
14221436
Elastic-Api-Version:
14231437
$ref: "#/components/headers/apiVersion"

pkg/api/client.gen.go

Lines changed: 20 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/api/types.gen.go

Lines changed: 13 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)