diff --git a/test/extensions/filters/http/common/fuzz/BUILD b/test/extensions/filters/http/common/fuzz/BUILD index 80066cf9eb40a..ffc67eb4a2321 100644 --- a/test/extensions/filters/http/common/fuzz/BUILD +++ b/test/extensions/filters/http/common/fuzz/BUILD @@ -23,6 +23,18 @@ envoy_proto_library( ], ) +envoy_cc_test_library( + name = "http_filter_fuzzer_lib", + hdrs = ["http_filter_fuzzer.h"], + deps = [ + "//include/envoy/http:filter_interface", + "//source/common/http:utility_lib", + "//test/fuzz:common_proto_cc_proto", + "//test/fuzz:utility_lib", + "//test/test_common:utility_lib", + ], +) + envoy_cc_test_library( name = "uber_filter_lib", srcs = [ @@ -32,13 +44,13 @@ envoy_cc_test_library( hdrs = ["uber_filter.h"], deps = [ ":filter_fuzz_proto_cc_proto", + ":http_filter_fuzzer_lib", "//source/common/config:utility_lib", "//source/common/http:utility_lib", "//source/common/protobuf:utility_lib", "//source/common/tracing:http_tracer_lib", "//source/extensions/filters/http:well_known_names", "//source/extensions/filters/http/common:utility_lib", - "//test/fuzz:utility_lib", "//test/mocks/buffer:buffer_mocks", "//test/mocks/http:http_mocks", "//test/mocks/server:factory_context_mocks", diff --git a/test/extensions/filters/http/common/fuzz/http_filter_fuzzer.h b/test/extensions/filters/http/common/fuzz/http_filter_fuzzer.h new file mode 100644 index 0000000000000..dba20989d343d --- /dev/null +++ b/test/extensions/filters/http/common/fuzz/http_filter_fuzzer.h @@ -0,0 +1,206 @@ +#pragma once + +#include "envoy/http/filter.h" + +#include "common/http/utility.h" + +#include "test/fuzz/common.pb.h" +#include "test/fuzz/utility.h" +#include "test/test_common/utility.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { + +// Generic library to fuzz HTTP filters. +// Usage: +// 1. Create filter and set callbacks. +// ExampleFilter filter; +// filter.setDecoderFilterCallbacks(decoder_callbacks); +// +// 2. Create HttpFilterFuzzer class and run decode methods. Optionally add access logging. Reset +// fuzzer to reset state. This class can be static. All state is reset in the reset method. +// Envoy::Extensions::HttpFilters::HttpFilterFuzzer fuzzer; +// fuzzer.runData(static_cast(&filter), +// input.downstream_request()); +// fuzzer.accessLog(static_cast(&filter), +// stream_info); +// fuzzer.reset(); + +class HttpFilterFuzzer { +public: + // Instantiate HttpFilterFuzzer + HttpFilterFuzzer() = default; + + // This executes the filter decode or encode methods with the fuzzed data. + template void runData(FilterType* filter, const test::fuzz::HttpData& data); + + // This executes the access logger with the fuzzed headers/trailers. + void accessLog(AccessLog::Instance* access_logger, const StreamInfo::StreamInfo& stream_info) { + ENVOY_LOG_MISC(debug, "Access logging"); + access_logger->log(&request_headers_, &response_headers_, &response_trailers_, stream_info); + } + + // Fuzzed headers and trailers are needed for access logging, reset the data and destroy filters. + void reset() { + enabled_ = true; + request_headers_.clear(); + response_headers_.clear(); + request_trailers_.clear(); + response_trailers_.clear(); + encoded_trailers_.clear(); + } + +protected: + // Templated functions to validate and send headers/data/trailers for decoders/encoders. + // General functions are deleted, but templated specializations for encoders/decoders are defined + // in the cc file. + template + Http::FilterHeadersStatus sendHeaders(FilterType* filter, const test::fuzz::HttpData& data, + bool end_stream) = delete; + + template + Http::FilterDataStatus sendData(FilterType* filter, Buffer::Instance& buffer, + bool end_stream) = delete; + + template + void sendTrailers(FilterType* filter, const test::fuzz::HttpData& data) = delete; + + // This keeps track of when a filter will stop decoding due to direct responses. + // If your filter needs to stop decoding because of a direct response, make sure you override + // sendLocalReply to set enabled_ to false. + bool enabled_ = true; + + // Headers/trailers need to be saved for the lifetime of the filter, + // so save them as member variables. + Http::TestRequestHeaderMapImpl request_headers_; + Http::TestResponseHeaderMapImpl response_headers_; + Http::TestRequestTrailerMapImpl request_trailers_; + Http::TestResponseTrailerMapImpl response_trailers_; + Http::TestResponseTrailerMapImpl encoded_trailers_; +}; + +template +void HttpFilterFuzzer::runData(FilterType* filter, const test::fuzz::HttpData& data) { + bool end_stream = false; + enabled_ = true; + if (data.body_case() == test::fuzz::HttpData::BODY_NOT_SET && !data.has_trailers()) { + end_stream = true; + } + const auto& headersStatus = sendHeaders(filter, data, end_stream); + ENVOY_LOG_MISC(debug, "Finished with FilterHeadersStatus: {}", headersStatus); + if ((headersStatus != Http::FilterHeadersStatus::Continue && + headersStatus != Http::FilterHeadersStatus::StopIteration) || + !enabled_) { + return; + } + + const std::vector data_chunks = Fuzz::parseHttpData(data); + for (size_t i = 0; i < data_chunks.size(); i++) { + if (!data.has_trailers() && i == data_chunks.size() - 1) { + end_stream = true; + } + Buffer::OwnedImpl buffer(data_chunks[i]); + const auto& dataStatus = sendData(filter, buffer, end_stream); + ENVOY_LOG_MISC(debug, "Finished with FilterDataStatus: {}", dataStatus); + if (dataStatus != Http::FilterDataStatus::Continue || !enabled_) { + return; + } + } + + if (data.has_trailers() && enabled_) { + sendTrailers(filter, data); + } +} + +template <> +inline Http::FilterHeadersStatus HttpFilterFuzzer::sendHeaders(Http::StreamDecoderFilter* filter, + const test::fuzz::HttpData& data, + bool end_stream) { + request_headers_ = Fuzz::fromHeaders(data.headers()); + if (request_headers_.Path() == nullptr) { + request_headers_.setPath("/foo"); + } + if (request_headers_.Method() == nullptr) { + request_headers_.setMethod("GET"); + } + if (request_headers_.Host() == nullptr) { + request_headers_.setHost("foo.com"); + } + + ENVOY_LOG_MISC(debug, "Decoding headers (end_stream={}):\n{} ", end_stream, request_headers_); + Http::FilterHeadersStatus status = filter->decodeHeaders(request_headers_, end_stream); + if (end_stream) { + filter->decodeComplete(); + } + return status; +} + +template <> +inline Http::FilterHeadersStatus HttpFilterFuzzer::sendHeaders(Http::StreamEncoderFilter* filter, + const test::fuzz::HttpData& data, + bool end_stream) { + response_headers_ = Fuzz::fromHeaders(data.headers()); + + // Status must be a valid unsigned long. If not set, the utility function below will throw + // an exception on the data path of some filters. This should never happen in production, so catch + // the exception and set to a default value. + try { + (void)Http::Utility::getResponseStatus(response_headers_); + } catch (const Http::CodecClientException& e) { + response_headers_.setStatus(200); + } + + ENVOY_LOG_MISC(debug, "Encoding headers (end_stream={}):\n{} ", end_stream, response_headers_); + Http::FilterHeadersStatus status = filter->encodeHeaders(response_headers_, end_stream); + if (end_stream) { + filter->encodeComplete(); + } + return status; +} + +template <> +inline Http::FilterDataStatus HttpFilterFuzzer::sendData(Http::StreamDecoderFilter* filter, + Buffer::Instance& buffer, + bool end_stream) { + ENVOY_LOG_MISC(debug, "Decoding data (end_stream={}): {} ", end_stream, buffer.toString()); + Http::FilterDataStatus status = filter->decodeData(buffer, end_stream); + if (end_stream) { + filter->decodeComplete(); + } + return status; +} + +template <> +inline Http::FilterDataStatus HttpFilterFuzzer::sendData(Http::StreamEncoderFilter* filter, + Buffer::Instance& buffer, + bool end_stream) { + ENVOY_LOG_MISC(debug, "Encoding data (end_stream={}): {} ", end_stream, buffer.toString()); + Http::FilterDataStatus status = filter->encodeData(buffer, end_stream); + if (end_stream) { + filter->encodeComplete(); + } + return status; +} + +template <> +inline void HttpFilterFuzzer::sendTrailers(Http::StreamDecoderFilter* filter, + const test::fuzz::HttpData& data) { + request_trailers_ = Fuzz::fromHeaders(data.trailers()); + ENVOY_LOG_MISC(debug, "Decoding trailers:\n{} ", request_trailers_); + filter->decodeTrailers(request_trailers_); + filter->decodeComplete(); +} + +template <> +inline void HttpFilterFuzzer::sendTrailers(Http::StreamEncoderFilter* filter, + const test::fuzz::HttpData& data) { + response_trailers_ = Fuzz::fromHeaders(data.trailers()); + ENVOY_LOG_MISC(debug, "Encoding trailers:\n{} ", response_trailers_); + filter->encodeTrailers(response_trailers_); + filter->encodeComplete(); +} + +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/common/fuzz/uber_filter.cc b/test/extensions/filters/http/common/fuzz/uber_filter.cc index 65924becd985d..0fcab5125e71e 100644 --- a/test/extensions/filters/http/common/fuzz/uber_filter.cc +++ b/test/extensions/filters/http/common/fuzz/uber_filter.cc @@ -54,138 +54,6 @@ UberFilterFuzzer::UberFilterFuzzer() : async_request_{&cluster_manager_.async_cl perFilterSetup(); } -std::vector UberFilterFuzzer::parseHttpData(const test::fuzz::HttpData& data) { - std::vector data_chunks; - - if (data.has_http_body()) { - data_chunks.reserve(data.http_body().data_size()); - for (const std::string& http_data : data.http_body().data()) { - data_chunks.push_back(http_data); - } - } else if (data.has_proto_body()) { - const std::string serialized = data.proto_body().message().value(); - data_chunks = absl::StrSplit(serialized, absl::ByLength(data.proto_body().chunk_size())); - } - - return data_chunks; -} - -template -void UberFilterFuzzer::runData(FilterType* filter, const test::fuzz::HttpData& data) { - bool end_stream = false; - enabled_ = true; - if (data.body_case() == test::fuzz::HttpData::BODY_NOT_SET && !data.has_trailers()) { - end_stream = true; - } - const auto& headersStatus = sendHeaders(filter, data, end_stream); - ENVOY_LOG_MISC(debug, "Finished with FilterHeadersStatus: {}", headersStatus); - if ((headersStatus != Http::FilterHeadersStatus::Continue && - headersStatus != Http::FilterHeadersStatus::StopIteration) || - !enabled_) { - return; - } - - const std::vector data_chunks = parseHttpData(data); - for (size_t i = 0; i < data_chunks.size(); i++) { - if (!data.has_trailers() && i == data_chunks.size() - 1) { - end_stream = true; - } - Buffer::OwnedImpl buffer(data_chunks[i]); - const auto& dataStatus = sendData(filter, buffer, end_stream); - ENVOY_LOG_MISC(debug, "Finished with FilterDataStatus: {}", dataStatus); - if (dataStatus != Http::FilterDataStatus::Continue || !enabled_) { - return; - } - } - - if (data.has_trailers() && enabled_) { - sendTrailers(filter, data); - } -} - -template <> -Http::FilterHeadersStatus UberFilterFuzzer::sendHeaders(Http::StreamDecoderFilter* filter, - const test::fuzz::HttpData& data, - bool end_stream) { - request_headers_ = Fuzz::fromHeaders(data.headers()); - if (request_headers_.Path() == nullptr) { - request_headers_.setPath("/foo"); - } - if (request_headers_.Method() == nullptr) { - request_headers_.setMethod("GET"); - } - if (request_headers_.Host() == nullptr) { - request_headers_.setHost("foo.com"); - } - - ENVOY_LOG_MISC(debug, "Decoding headers (end_stream={}):\n{} ", end_stream, request_headers_); - return filter->decodeHeaders(request_headers_, end_stream); -} - -template <> -Http::FilterHeadersStatus UberFilterFuzzer::sendHeaders(Http::StreamEncoderFilter* filter, - const test::fuzz::HttpData& data, - bool end_stream) { - response_headers_ = Fuzz::fromHeaders(data.headers()); - - // Status must be a valid unsigned long. If not set, the utility function below will throw - // an exception on the data path of some filters. This should never happen in production, so catch - // the exception and set to a default value. - try { - (void)Http::Utility::getResponseStatus(response_headers_); - } catch (const Http::CodecClientException& e) { - response_headers_.setStatus(200); - } - - ENVOY_LOG_MISC(debug, "Encoding headers (end_stream={}):\n{} ", end_stream, response_headers_); - Http::FilterHeadersStatus status = filter->encodeHeaders(response_headers_, end_stream); - if (end_stream) { - filter->encodeComplete(); - } - return status; -} - -template <> -Http::FilterDataStatus UberFilterFuzzer::sendData(Http::StreamDecoderFilter* filter, - Buffer::Instance& buffer, bool end_stream) { - ENVOY_LOG_MISC(debug, "Decoding data (end_stream={}): {} ", end_stream, buffer.toString()); - return filter->decodeData(buffer, end_stream); -} - -template <> -Http::FilterDataStatus UberFilterFuzzer::sendData(Http::StreamEncoderFilter* filter, - Buffer::Instance& buffer, bool end_stream) { - ENVOY_LOG_MISC(debug, "Encoding data (end_stream={}): {} ", end_stream, buffer.toString()); - Http::FilterDataStatus status = filter->encodeData(buffer, end_stream); - if (end_stream) { - filter->encodeComplete(); - } - return status; -} - -template <> -void UberFilterFuzzer::sendTrailers(Http::StreamDecoderFilter* filter, - const test::fuzz::HttpData& data) { - request_trailers_ = Fuzz::fromHeaders(data.trailers()); - ENVOY_LOG_MISC(debug, "Decoding trailers:\n{} ", request_trailers_); - filter->decodeTrailers(request_trailers_); -} - -template <> -void UberFilterFuzzer::sendTrailers(Http::StreamEncoderFilter* filter, - const test::fuzz::HttpData& data) { - response_trailers_ = Fuzz::fromHeaders(data.trailers()); - ENVOY_LOG_MISC(debug, "Encoding trailers:\n{} ", response_trailers_); - filter->encodeTrailers(response_trailers_); - filter->encodeComplete(); -} - -void UberFilterFuzzer::accessLog(AccessLog::Instance* access_logger, - const StreamInfo::StreamInfo& stream_info) { - ENVOY_LOG_MISC(debug, "Access logging"); - access_logger->log(&request_headers_, &response_headers_, &response_trailers_, stream_info); -} - void UberFilterFuzzer::fuzz( const envoy::extensions::filters::network::http_connection_manager::v3::HttpFilter& proto_config, @@ -208,13 +76,13 @@ void UberFilterFuzzer::fuzz( // Data path should not throw exceptions. if (decoder_filter_ != nullptr) { - runData(decoder_filter_.get(), downstream_data); + HttpFilterFuzzer::runData(decoder_filter_.get(), downstream_data); } if (encoder_filter_ != nullptr) { - runData(encoder_filter_.get(), upstream_data); + HttpFilterFuzzer::runData(encoder_filter_.get(), upstream_data); } if (access_logger_ != nullptr) { - accessLog(access_logger_.get(), stream_info_); + HttpFilterFuzzer::accessLog(access_logger_.get(), stream_info_); } reset(); @@ -232,11 +100,7 @@ void UberFilterFuzzer::reset() { encoder_filter_.reset(); access_logger_.reset(); - request_headers_.clear(); - response_headers_.clear(); - request_trailers_.clear(); - response_trailers_.clear(); - encoded_trailers_.clear(); + HttpFilterFuzzer::reset(); } } // namespace HttpFilters diff --git a/test/extensions/filters/http/common/fuzz/uber_filter.h b/test/extensions/filters/http/common/fuzz/uber_filter.h index 4d1a894fef1e5..8bcef67d6afa9 100644 --- a/test/extensions/filters/http/common/fuzz/uber_filter.h +++ b/test/extensions/filters/http/common/fuzz/uber_filter.h @@ -1,3 +1,4 @@ +#include "test/extensions/filters/http/common/fuzz/http_filter_fuzzer.h" #include "test/fuzz/utility.h" #include "test/mocks/buffer/mocks.h" #include "test/mocks/http/mocks.h" @@ -8,7 +9,8 @@ namespace Envoy { namespace Extensions { namespace HttpFilters { -class UberFilterFuzzer { +// Generic filter fuzzer that can fuzz any HttpFilter. +class UberFilterFuzzer : public HttpFilterFuzzer { public: UberFilterFuzzer(); @@ -17,16 +19,9 @@ class UberFilterFuzzer { proto_config, const test::fuzz::HttpData& downstream_data, const test::fuzz::HttpData& upstream_data); - // This executes the filter decoders/encoders with the fuzzed data. - template void runData(FilterType* filter, const test::fuzz::HttpData& data); - - // This executes the access logger with the fuzzed headers/trailers. - void accessLog(AccessLog::Instance* access_logger, const StreamInfo::StreamInfo& stream_info); - // For fuzzing proto data, guide the mutator to useful 'Any' types. static void guideAnyProtoType(test::fuzz::HttpData* mutable_data, uint choice); - // Resets cached data (request headers, etc.). Should be called for each fuzz iteration. void reset(); protected: @@ -35,26 +30,7 @@ class UberFilterFuzzer { // Filter specific input cleanup. void cleanFuzzedConfig(absl::string_view filter_name, Protobuf::Message* message); - // Parses http or proto body into chunks. - static std::vector parseHttpData(const test::fuzz::HttpData& data); - - // Templated functions to validate and send headers/data/trailers for decoders/encoders. - // General functions are deleted, but templated specializations for encoders/decoders are defined - // in the cc file. - template - Http::FilterHeadersStatus sendHeaders(FilterType* filter, const test::fuzz::HttpData& data, - bool end_stream) = delete; - - template - Http::FilterDataStatus sendData(FilterType* filter, Buffer::Instance& buffer, - bool end_stream) = delete; - - template - void sendTrailers(FilterType* filter, const test::fuzz::HttpData& data) = delete; - private: - // This keeps track of when a filter will stop decoding due to direct responses. - bool enabled_ = true; NiceMock factory_context_; NiceMock filter_callback_; std::shared_ptr resolver_{std::make_shared()}; @@ -65,23 +41,14 @@ class UberFilterFuzzer { NiceMock async_request_; NiceMock stream_info_; - // Mocked callbacks. - NiceMock decoder_callbacks_; - NiceMock encoder_callbacks_; - // Filter constructed from the config. Http::StreamDecoderFilterSharedPtr decoder_filter_; Http::StreamEncoderFilterSharedPtr encoder_filter_; AccessLog::InstanceSharedPtr access_logger_; - // Headers/trailers need to be saved for the lifetime of the filter, - // so save them as member variables. - // TODO(nareddyt): Use for access logging in a followup PR. - Http::TestRequestHeaderMapImpl request_headers_; - Http::TestResponseHeaderMapImpl response_headers_; - Http::TestRequestTrailerMapImpl request_trailers_; - Http::TestResponseTrailerMapImpl response_trailers_; - Http::TestResponseTrailerMapImpl encoded_trailers_; + // Mocked callbacks. + NiceMock decoder_callbacks_; + NiceMock encoder_callbacks_; }; } // namespace HttpFilters diff --git a/test/fuzz/utility.h b/test/fuzz/utility.h index f79aece1041bf..332e6ae01fce6 100644 --- a/test/fuzz/utility.h +++ b/test/fuzz/utility.h @@ -175,5 +175,22 @@ inline std::unique_ptr fromStreamInfo(const test::fuzz::StreamIn return test_stream_info; } +// Parses http or proto body into chunks. +inline std::vector parseHttpData(const test::fuzz::HttpData& data) { + std::vector data_chunks; + + if (data.has_http_body()) { + data_chunks.reserve(data.http_body().data_size()); + for (const std::string& http_data : data.http_body().data()) { + data_chunks.push_back(http_data); + } + } else if (data.has_proto_body()) { + const std::string serialized = data.proto_body().message().value(); + data_chunks = absl::StrSplit(serialized, absl::ByLength(data.proto_body().chunk_size())); + } + + return data_chunks; +} + } // namespace Fuzz } // namespace Envoy