Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

compression: add generic decompressor filter #11172

Merged
merged 65 commits into from
May 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
120e412
dont delete my proto files
Apr 10, 2020
ac30003
versioning stuff
Apr 10, 2020
de750ce
option
Apr 10, 2020
9df2154
proto: final state approved by formatter
Apr 10, 2020
ab9a8f6
create new extension point in api
Apr 10, 2020
6b37345
CHECKPOINT: moved protos to decompressors extension point
Apr 10, 2020
b02ba42
decompressors extension point and gzip implementation. built and form…
Apr 11, 2020
7ecc87a
spelling
Apr 11, 2020
68929fe
move protos again
Apr 11, 2020
222be60
new tree
Apr 11, 2020
4be8a3e
done up to decompressor
Apr 11, 2020
33c7ba1
fmt
Apr 11, 2020
9f86578
name
Apr 11, 2020
f6d4737
minor
Apr 12, 2020
21b62e0
name
Apr 13, 2020
a4a02bb
wip
Apr 16, 2020
4df55ff
wip
Apr 16, 2020
6e1415f
move back to mac
Apr 16, 2020
9de0fd0
more tests
Apr 16, 2020
3ab576d
passing
Apr 16, 2020
87fc130
segfault
Apr 16, 2020
a26ae04
pass
Apr 16, 2020
8faedf3
segfault
Apr 16, 2020
b45e113
passed
Apr 16, 2020
1a981cf
tests complete, look at coverage
Apr 17, 2020
4830068
all code covered
Apr 17, 2020
1043607
fmt
Apr 18, 2020
4bca431
Merge branch 'master' into decompressor-filter
Apr 30, 2020
9366f99
update
Apr 30, 2020
b37b163
comments
May 4, 2020
ba0e4e0
Merge branch 'master' into decompressor-filter
May 8, 2020
886a05b
fmt
May 8, 2020
6fff9ea
update
May 10, 2020
818e2c9
fmt
May 10, 2020
2859297
fix
May 11, 2020
5ed1ed7
last fix
May 11, 2020
0fbf46d
docs
May 11, 2020
021ae5b
docs
May 11, 2020
a80dcc0
missing generated proto
May 11, 2020
bed19db
Merge branch 'decompressor-filter' into add-decompressor-filter
May 11, 2020
f176973
wip going to merge master
May 12, 2020
06c6792
Merge branch 'master' into add-decompressor-filter
May 12, 2020
c98cf66
update
May 12, 2020
295d7f9
green
May 12, 2020
501efed
spelling
May 12, 2020
bf576da
spelling
May 12, 2020
61daf12
progress
May 13, 2020
4ab7a83
buffering, missing tests
May 13, 2020
18cb24f
variant
May 13, 2020
feb6228
test wip
May 13, 2020
cd9cd9b
buffer bytes
May 13, 2020
c5c6738
unit tests complete
May 13, 2020
d3311b2
window bits
May 14, 2020
42de78b
remove buffering
May 14, 2020
20a5cf4
opt ref
May 14, 2020
6fe0828
fmt
May 15, 2020
bbb8370
some comments
May 19, 2020
a3521a2
comments
May 19, 2020
9eeba7a
spelling
May 19, 2020
513fcff
spelling
May 19, 2020
4862341
finishing details: docs, release, integration test
May 20, 2020
df7f12f
spelling
May 20, 2020
8127a66
comments
May 21, 2020
1bf8156
fmt
May 21, 2020
858c279
update docs
May 25, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,4 @@ extensions/filters/common/original_src @snowp @klarose
# Compression
/*/extensions/compression/common @junr03 @rojkov
/*/extensions/compression/gzip @junr03 @rojkov
/*/extensions/filters/http/decompressor @rojkov @dio
2 changes: 1 addition & 1 deletion STYLE.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
* Regular pointers (e.g. `int* foo`) should not be type aliased.
* `absl::optional<std::reference_wrapper<T>> is type aliased:
* `using FooOptRef = absl::optional<std::reference_wrapper<T>>;`
* `using FooOptConstRef = absl::optional<std::reference_wrapper<T>>;`
* `using FooOptConstRef = absl::optional<std::reference_wrapper<const T>>;`
* If move semantics are intended, prefer specifying function arguments with `&&`.
E.g., `void onHeaders(Http::HeaderMapPtr&& headers, ...)`. The rationale for this is that it
forces the caller to specify `std::move(...)` or pass a temporary and makes the intention at
Expand Down
1 change: 1 addition & 0 deletions api/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ proto_library(
"//envoy/extensions/filters/http/compressor/v3:pkg",
"//envoy/extensions/filters/http/cors/v3:pkg",
"//envoy/extensions/filters/http/csrf/v3:pkg",
"//envoy/extensions/filters/http/decompressor/v3:pkg",
"//envoy/extensions/filters/http/dynamic_forward_proxy/v3:pkg",
"//envoy/extensions/filters/http/dynamo/v3:pkg",
"//envoy/extensions/filters/http/ext_authz/v3:pkg",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ message Compressor {
config.core.v3.RuntimeFeatureFlag runtime_enabled = 5;

// A compressor library to use for compression. Currently only
// :ref:`envoy.filters.http.compressor.gzip<envoy_api_msg_extensions.compression.gzip.compressor.v3.Gzip>`
// :ref:`envoy.compression.gzip.compressor<envoy_api_msg_extensions.compression.gzip.compressor.v3.Gzip>`
// is included in Envoy.
// This field is ignored if used in the context of the gzip http-filter, but is mandatory otherwise.
config.core.v3.TypedExtensionConfig compressor_library = 6;
Expand Down
12 changes: 12 additions & 0 deletions api/envoy/extensions/filters/http/decompressor/v3/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# DO NOT EDIT. This file is generated by tools/proto_sync.py.

load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package")

licenses(["notice"]) # Apache 2

api_proto_package(
deps = [
"//envoy/config/core/v3:pkg",
"@com_github_cncf_udpa//udpa/annotations:pkg",
],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
syntax = "proto3";

package envoy.extensions.filters.http.decompressor.v3;

import "envoy/config/core/v3/base.proto";
import "envoy/config/core/v3/extension.proto";

import "google/protobuf/any.proto";
import "google/protobuf/wrappers.proto";

import "udpa/annotations/migrate.proto";
import "udpa/annotations/status.proto";
import "udpa/annotations/versioning.proto";
import "validate/validate.proto";

option java_package = "io.envoyproxy.envoy.extensions.filters.http.decompressor.v3";
option java_outer_classname = "DecompressorProto";
option java_multiple_files = true;
option (udpa.annotations.file_status).package_version_status = ACTIVE;

// [#protodoc-title: Decompressor]
// [#extension: envoy.filters.http.decompressor]

message Decompressor {
// Common configuration for filter behavior on both the request and response direction.
message CommonDirectionConfig {
// Runtime flag that controls whether the filter is enabled for decompression or not. If set to false, the
// filter will operate as a pass-through filter. If the message is unspecified, the filter will be enabled.
config.core.v3.RuntimeFeatureFlag enabled = 1;
}

// Configuration for filter behavior on the request direction.
message RequestDirectionConfig {
CommonDirectionConfig common_config = 1;

// If set to true, and response decompression is enabled, the filter modifies the Accept-Encoding
// request header by appending the decompressor_library's encoding. Defaults to true.
google.protobuf.BoolValue advertise_accept_encoding = 2;
}

// Configuration for filter behavior on the response direction.
message ResponseDirectionConfig {
CommonDirectionConfig common_config = 1;
}

// A decompressor library to use for both request and response decompression. Currently only
// :ref:`envoy.compression.gzip.compressor<envoy_api_msg_extensions.compression.gzip.decompressor.v3.Gzip>`
// is included in Envoy.
config.core.v3.TypedExtensionConfig decompressor_library = 1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we configure bufferization separately for requests and responses shouldn't we have separate configs for the decompressor library as well?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could do that. And the stats would work as well.... I am not sure if that is increasing the cognitive load of someone trying to configure more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was my concern too: that might surprise if some options are configured separately and some are not. I'd probably prefer to configure two separate decompressor filters configured differently for different directions. But now with bufferization moved out it looks much better. I like it.

Perhaps the message's hierarchy could be flatter with request_direction_config.common_config.advertise_accept_encoding changed to just request_advertise_accept_encoding. Though I don't have strong opinion.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'll leave as is to future proof in case people want to add more complexity.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW it seemed odd to me that we don't allow this to be configured differently in each direction? Can you at least clarify this applies to both the request and response configuration depending on whether they are configured or not?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's trivial to configure them independently, i.e allow different decompressors on each direction. If both you and @rojkov like the idea of allowing that, I can make the change.

Copy link
Member Author

@junr03 junr03 May 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM modulo the comment about the compression config. Not sure if you are still planning on fixing that or not.

@mattklein123 do you mean this thread? Sorry, I was waiting for you and/or @rojkov to vote one way or another. But I guess both of you have already expressed interest in it, so I will go ahead and add.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO it's a question of composability vs monolithic design. As a proponent of the former I'd prefer two sequential decompressor filters configured differently for different directions and only consider the latter if performance became an issue.

The same applies to buffering: coupled with the buffer filters we can get the same functionality with less code complexity. If the buffer filter is limited to requests only then it makes sense to extend it (and only it) - people might want to buffer responses in scenarios other than decompression (e.g. compression).

So, for now I vote for a comment on how to couple filters for various use cases instead of complicating the filter. Later we can reconsider.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine either way as long as the current state is well documented.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, I had added documentation in the proto, but I have expanded with an example. The current proto would lend itself pretty easily to adding per direction config in the future is someone wants it.

lmk if you think this is clear @mattklein123 @rojkov 858c279

[(validate.rules).message = {required: true}];

// Configuration for request decompression. Decompression is enabled by default if left empty.
RequestDirectionConfig request_direction_config = 2;

// Configuration for response decompression. Decompression is enabled by default if left empty.
ResponseDirectionConfig response_direction_config = 3;
}
1 change: 1 addition & 0 deletions api/versioning/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ proto_library(
"//envoy/extensions/filters/http/compressor/v3:pkg",
"//envoy/extensions/filters/http/cors/v3:pkg",
"//envoy/extensions/filters/http/csrf/v3:pkg",
"//envoy/extensions/filters/http/decompressor/v3:pkg",
"//envoy/extensions/filters/http/dynamic_forward_proxy/v3:pkg",
"//envoy/extensions/filters/http/dynamo/v3:pkg",
"//envoy/extensions/filters/http/ext_authz/v3:pkg",
Expand Down
112 changes: 112 additions & 0 deletions docs/root/configuration/http/http_filters/decompressor_filter.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
.. _config_http_filters_decompressor:

Decompressor
============
Decompressor is an HTTP filter which enables Envoy to bidirectionally decompress data.


Configuration
-------------
* :ref:`v3 API reference <envoy_v3_api_msg_extensions.filters.http.decompressor.v3.Decompressor>`

How it works
------------
When the decompressor filter is enabled, headers are inspected to
determine whether or not the content should be decompressed. The content is
decompressed and passed on to the rest of the filter chain. Note that decompression happens
independently for request and responses based on the rules described below.

Currently the filter supports :ref:`gzip compression <envoy_v3_api_msg_extensions.compression.gzip.decompressor.v3.Gzip>`
only. Other compression libraries can be supported as extensions.

An example configuration of the filter may look like the following:

.. code-block:: yaml

http_filters:
- name: decompressor
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.decompressor.v3.Decompressor
decompressor_library:
name: basic
typed_config:
"@type": type.googleapis.com/envoy.extensions.compression.gzip.decompressor.v3.Gzip
window_bits: 10

By *default* decompression will be *skipped* when:

- A request/response does NOT contain *content-encoding* header.
- A request/response includes *content-encoding* header, but it does not contain the configured
decompressor's content-encoding.
- A request/response contains a *cache-control* header whose value includes "no-transform".

When decompression is *applied*:

- The *content-length* is removed from headers.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this going to break some uses cases? Is it a future work item that we might want to buffer the decompressed data and rewrite content-length?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, sorry @mattklein123 , this context got dropped because github didn't allow me to move my original PR over.

We discussed here: https://github.com/junr03/envoy/pull/1#discussion_r411669576

And then I had this idea: #11172 (comment)

tl;dr: I originally implemented buffering here. But then decided to remove (and depend on the buffer filter) to keep this filter simpler. wdyt?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the buffer filter reset content length? Anyway I think that's fine but I might make that clear in the documentation that the user might want to couple this with the buffer filter.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the buffer filter reset content length?

Yep, it sets it if not present, which made it ideal for this arrangement.

void BufferFilter::maybeAddContentLength() {

Yeah I can add a section in the docs about it 👌

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ultimately the issue is that the setup would still be a bit kludgy because of filter ordering, and the fact that the buffer filter can't be disabled independently for requests and responses. For Lyft's use case it'll be fine because we don't need to preserve content-length on responses.

So at the end of the day it might make sense to have this filter pay the price and do buffering itself. My proposal is to leave as is (with the new docs I added) to get this PR in, and then I can open an issue with a link to the commit that had buffering, it would just need to be updated a bit, and integration tests written (which shouldn't be hard at all). I can get to it as a low-pri.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine to document the limitation and we can revisit later.


.. note::

If an updated *content-length* header is desired, the buffer filter can be installed as part
of the filter chain to buffer decompressed frames, and ultimately update the header. Due to
:ref:`filter ordering <arch_overview_http_filters_ordering>` a buffer filter needs to be
installed after the decompressor for requests and prior to the decompressor for responses.

- The *content-encoding* header is modified to remove the decompression that was applied.

.. _decompressor-statistics:

Using different decompressors for requests and responses
--------------------------------------------------------

If different compression libraries are desired for requests and responses, it is possible to install
multiple decompressor filters enabled only for requests or responses. For instance:

.. code-block:: yaml

http_filters:
# This filter is only enabled for requests.
- name: envoy.filters.http.decompressor
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.decompressor.v3.Decompressor
decompressor_library:
name: small
typed_config:
"@type": "type.googleapis.com/envoy.extensions.compression.gzip.decompressor.v3.Gzip"
window_bits: 9
chunk_size: 8192
response_direction_config:
common_config:
enabled:
default_value: false
runtime_key: response_decompressor_enabled
# This filter is only enabled for responses.
- name: envoy.filters.http.decompressor
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.decompressor.v3.Decompressor
decompressor_library:
name: large
typed_config:
"@type": "type.googleapis.com/envoy.extensions.compression.gzip.decompressor.v3.Gzip"
window_bits: 12
chunk_size: 16384
request_direction_config:
common_config:
enabled:
default_value: false
runtime_key: request_decompressor_enabled

Statistics
----------

Every configured Deompressor filter has statistics rooted at
<stat_prefix>.decompressor.<decompressor_library.name>.<decompressor_library_stat_prefix>.<request/response>*
with the following:

.. csv-table::
:header: Name, Type, Description
:widths: 1, 1, 2

decompressed, Counter, Number of request/responses compressed.
not_decompressed, Counter, Number of request/responses not compressed.
total_uncompressed_bytes, Counter, The total uncompressed bytes of all the request/responses that were marked for decompression.
total_compressed_bytes, Counter, The total compressed bytes of all the request/responses that were marked for decompression.
1 change: 1 addition & 0 deletions docs/root/configuration/http/http_filters/http_filters.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ HTTP filters
compressor_filter
cors_filter
csrf_filter
decompressor_filter
dynamic_forward_proxy_filter
dynamodb_filter
ext_authz_filter
Expand Down
1 change: 1 addition & 0 deletions docs/root/version_history/current.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Changes
* build: official released binary is now built on Ubuntu 18.04, requires glibc >= 2.27.
* compressor: generic :ref:`compressor <config_http_filters_compressor>` filter exposed to users.
* config: added :ref:`version_text <config_cluster_manager_cds>` stat that reflects xDS version.
* decompressor: generic :ref:`decompressor <config_http_filters_decompressor>` filter exposed to users.
* dynamic forward proxy: added :ref:`SNI based dynamic forward proxy <config_network_filters_sni_dynamic_forward_proxy>` support.
* fault: added support for controlling the percentage of requests that abort, delay and response rate limits faults
are applied to using :ref:`HTTP headers <config_http_filters_fault_injection_http_header>` to the HTTP fault filter.
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion include/envoy/http/header_map.h
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,6 @@ class HeaderEntry {
HEADER_FUNC(AccessControlAllowOrigin) \
HEADER_FUNC(AccessControlExposeHeaders) \
HEADER_FUNC(AccessControlMaxAge) \
HEADER_FUNC(ContentEncoding) \
HEADER_FUNC(Date) \
HEADER_FUNC(Etag) \
HEADER_FUNC(EnvoyDegraded) \
Expand All @@ -346,6 +345,7 @@ class HeaderEntry {
#define INLINE_REQ_RESP_HEADERS(HEADER_FUNC) \
HEADER_FUNC(CacheControl) \
HEADER_FUNC(Connection) \
HEADER_FUNC(ContentEncoding) \
HEADER_FUNC(ContentLength) \
HEADER_FUNC(ContentType) \
HEADER_FUNC(EnvoyAttemptCount) \
Expand Down
3 changes: 2 additions & 1 deletion source/extensions/compression/gzip/compressor/config.cc
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ const uint64_t DefaultMemoryLevel = 5;
// Default and maximum compression window size.
const uint64_t DefaultWindowBits = 12;

// When summed to window bits, this sets a gzip header and trailer around the compressed data.
// When logical OR'ed to window bits, this sets a gzip header and trailer around the compressed
// data.
const uint64_t GzipHeaderValue = 16;

// Default zlib chunk size.
Expand Down
6 changes: 5 additions & 1 deletion source/extensions/compression/gzip/decompressor/config.cc
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@ namespace Decompressor {
namespace {
const uint32_t DefaultWindowBits = 12;
const uint32_t DefaultChunkSize = 4096;
// When logical OR'ed to window bits, this tells zlib library to decompress gzip data per:
// inflateInit2 in https://www.zlib.net/manual.html
const uint32_t GzipHeaderValue = 16;
} // namespace

GzipDecompressorFactory::GzipDecompressorFactory(
const envoy::extensions::compression::gzip::decompressor::v3::Gzip& gzip)
: window_bits_(PROTOBUF_GET_WRAPPED_OR_DEFAULT(gzip, window_bits, DefaultWindowBits)),
: window_bits_(PROTOBUF_GET_WRAPPED_OR_DEFAULT(gzip, window_bits, DefaultWindowBits) |
GzipHeaderValue),
chunk_size_(PROTOBUF_GET_WRAPPED_OR_DEFAULT(gzip, chunk_size, DefaultChunkSize)) {}

Envoy::Compression::Decompressor::DecompressorPtr GzipDecompressorFactory::createDecompressor() {
Expand Down
1 change: 1 addition & 0 deletions source/extensions/extensions_build_config.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ EXTENSIONS = {
"envoy.filters.http.compressor": "//source/extensions/filters/http/compressor:config",
"envoy.filters.http.cors": "//source/extensions/filters/http/cors:config",
"envoy.filters.http.csrf": "//source/extensions/filters/http/csrf:config",
"envoy.filters.http.decompressor": "//source/extensions/filters/http/decompressor:config",
"envoy.filters.http.dynamic_forward_proxy": "//source/extensions/filters/http/dynamic_forward_proxy:config",
"envoy.filters.http.dynamo": "//source/extensions/filters/http/dynamo:config",
"envoy.filters.http.ext_authz": "//source/extensions/filters/http/ext_authz:config",
Expand Down
Loading