1
#include "source/extensions/compression/brotli/decompressor/brotli_decompressor_impl.h"
2

            
3
#include <memory>
4

            
5
#include "source/common/runtime/runtime_features.h"
6

            
7
namespace Envoy {
8
namespace Extensions {
9
namespace Compression {
10
namespace Brotli {
11
namespace Decompressor {
12

            
13
namespace {
14

            
15
// How many times the output buffer is allowed to be bigger than the input
16
// buffer. This value is used to detect compression bombs.
17
// TODO(rojkov): Re-design the Decompressor interface to handle compression
18
// bombs gracefully instead of this quick solution.
19
constexpr uint32_t MaxInflateRatio = 100;
20

            
21
} // namespace
22

            
23
BrotliDecompressorImpl::BrotliDecompressorImpl(Stats::Scope& scope, const std::string& stats_prefix,
24
                                               const uint32_t chunk_size,
25
                                               const bool disable_ring_buffer_reallocation)
26
95
    : chunk_size_{chunk_size},
27
95
      state_(BrotliDecoderCreateInstance(nullptr, nullptr, nullptr), &BrotliDecoderDestroyInstance),
28
95
      stats_(generateStats(stats_prefix, scope)) {
29
95
  BROTLI_BOOL result =
30
95
      BrotliDecoderSetParameter(state_.get(), BROTLI_DECODER_PARAM_DISABLE_RING_BUFFER_REALLOCATION,
31
95
                                disable_ring_buffer_reallocation ? BROTLI_TRUE : BROTLI_FALSE);
32
95
  RELEASE_ASSERT(result == BROTLI_TRUE, "");
33
95
}
34

            
35
void BrotliDecompressorImpl::decompress(const Buffer::Instance& input_buffer,
36
95
                                        Buffer::Instance& output_buffer) {
37
95
  Common::BrotliContext ctx(chunk_size_, MaxInflateRatio * input_buffer.length());
38

            
39
3669
  for (const Buffer::RawSlice& input_slice : input_buffer.getRawSlices()) {
40
3669
    ctx.avail_in_ = input_slice.len_;
41
3669
    ctx.next_in_ = static_cast<uint8_t*>(input_slice.mem_);
42

            
43
17990
    while (ctx.avail_in_ > 0) {
44
14323
      if (!process(ctx, output_buffer)) {
45
2
        ctx.finalizeOutput(output_buffer);
46
2
        return;
47
2
      }
48
14323
    }
49
3669
  }
50

            
51
  // Even though the input has been fully consumed by the decoder it still can
52
  // be unfolded into output not fitting the output chunk. Thus keep processing
53
  // until the decoder's output is fully depleted.
54
93
  bool success;
55
12069
  do {
56
12069
    success = process(ctx, output_buffer);
57
12069
  } while (success && BrotliDecoderHasMoreOutput(state_.get()));
58

            
59
93
  ctx.finalizeOutput(output_buffer);
60
93
}
61

            
62
26392
bool BrotliDecompressorImpl::process(Common::BrotliContext& ctx, Buffer::Instance& output_buffer) {
63
26392
  BrotliDecoderResult result = BrotliDecoderDecompressStream(
64
26392
      state_.get(), &ctx.avail_in_, &ctx.next_in_, &ctx.avail_out_, &ctx.next_out_, nullptr);
65

            
66
26392
  switch (result) {
67
100
  case BROTLI_DECODER_RESULT_SUCCESS:
68
    // The decompression is done successfully but there is still some input left.
69
    // We treat this as an error and stop the decompression directly to avoid
70
    // possible endless loop.
71
100
    if (ctx.avail_in_ > 0) {
72
1
      stats_.brotli_error_.inc();
73
1
      stats_.brotli_redundant_input_.inc();
74
1
      return false;
75
1
    }
76
    // The decompression is done successfully and fall through to the next case
77
    // to check if the output buffer is full and flush chunk to the output buffer.
78
99
    FALLTHRU;
79
3717
  case BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT:
80
3717
    ASSERT(ctx.avail_in_ == 0);
81
3717
    FALLTHRU;
82
26390
  case BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT:
83
    // Check if the output buffer is full first. If it is full then we treat it
84
    // as an error and stop the decompression directly to avoid possible decompression
85
    // bomb.
86
26390
    if (Runtime::runtimeFeatureEnabled(
87
26390
            "envoy.reloadable_features.enable_compression_bomb_protection") &&
88
26390
        (output_buffer.length() > ctx.max_output_size_)) {
89
1
      stats_.brotli_error_.inc();
90
1
      stats_.brotli_output_overflow_.inc();
91
1
      return false;
92
1
    }
93

            
94
    // If current chunk is full then flush it to the output buffer and reset
95
    // the chunk or do nothing.
96
26389
    ctx.updateOutput(output_buffer);
97
26389
    return true;
98
1
  case BROTLI_DECODER_RESULT_ERROR:
99
1
    stats_.brotli_error_.inc();
100
1
    return false;
101
26392
  }
102

            
103
  PANIC("Unexpected BrotliDecoderResult");
104
}
105

            
106
} // namespace Decompressor
107
} // namespace Brotli
108
} // namespace Compression
109
} // namespace Extensions
110
} // namespace Envoy