| #include "http2/adapter/test_utils.h" |
| |
| #include "http2/adapter/nghttp2_util.h" |
| #include "common/quiche_endian.h" |
| #include "spdy/core/spdy_frame_reader.h" |
| |
| namespace http2 { |
| namespace adapter { |
| namespace test { |
| |
| TestDataFrameSource::TestDataFrameSource(Http2VisitorInterface& visitor, |
| absl::string_view data_payload, |
| bool has_fin) |
| : visitor_(visitor), has_fin_(has_fin) { |
| if (!data_payload.empty()) { |
| payload_fragments_.push_back(std::string(data_payload)); |
| current_fragment_ = payload_fragments_.front(); |
| } |
| } |
| |
| TestDataFrameSource::TestDataFrameSource( |
| Http2VisitorInterface& visitor, |
| absl::Span<absl::string_view> payload_fragments, |
| bool has_fin) |
| : visitor_(visitor), has_fin_(has_fin) { |
| payload_fragments_.reserve(payload_fragments.size()); |
| for (absl::string_view fragment : payload_fragments) { |
| if (!fragment.empty()) { |
| payload_fragments_.push_back(std::string(fragment)); |
| } |
| } |
| if (!payload_fragments_.empty()) { |
| current_fragment_ = payload_fragments_.front(); |
| } |
| } |
| |
| std::pair<ssize_t, bool> TestDataFrameSource::SelectPayloadLength( |
| size_t max_length) { |
| if (!is_data_available_) { |
| return {kBlocked, false}; |
| } |
| // The stream is done if there's no more data, or if |max_length| is at least |
| // as large as the remaining data. |
| const bool end_data = |
| current_fragment_.empty() || (payload_fragments_.size() == 1 && |
| max_length >= current_fragment_.size()); |
| const ssize_t length = std::min(max_length, current_fragment_.size()); |
| return {length, end_data}; |
| } |
| |
| bool TestDataFrameSource::Send(absl::string_view frame_header, |
| size_t payload_length) { |
| QUICHE_LOG_IF(DFATAL, payload_length > current_fragment_.size()) |
| << "payload_length: " << payload_length |
| << " current_fragment_size: " << current_fragment_.size(); |
| const std::string concatenated = |
| absl::StrCat(frame_header, current_fragment_.substr(0, payload_length)); |
| const ssize_t result = visitor_.OnReadyToSend(concatenated); |
| if (result < 0) { |
| // Write encountered error. |
| visitor_.OnConnectionError(); |
| current_fragment_ = {}; |
| payload_fragments_.clear(); |
| return false; |
| } else if (result == 0) { |
| // Write blocked. |
| return false; |
| } else if (result < concatenated.size()) { |
| // Probably need to handle this better within this test class. |
| QUICHE_LOG(DFATAL) |
| << "DATA frame not fully flushed. Connection will be corrupt!"; |
| visitor_.OnConnectionError(); |
| current_fragment_ = {}; |
| payload_fragments_.clear(); |
| return false; |
| } |
| current_fragment_.remove_prefix(payload_length); |
| if (current_fragment_.empty()) { |
| payload_fragments_.erase(payload_fragments_.begin()); |
| if (!payload_fragments_.empty()) { |
| current_fragment_ = payload_fragments_.front(); |
| } |
| } |
| return true; |
| } |
| |
| namespace { |
| |
| using TypeAndOptionalLength = |
| std::pair<spdy::SpdyFrameType, absl::optional<size_t>>; |
| |
| std::vector<std::pair<const char*, std::string>> LogFriendly( |
| const std::vector<TypeAndOptionalLength>& types_and_lengths) { |
| std::vector<std::pair<const char*, std::string>> out; |
| out.reserve(types_and_lengths.size()); |
| for (const auto type_and_length : types_and_lengths) { |
| out.push_back({spdy::FrameTypeToString(type_and_length.first), |
| type_and_length.second |
| ? absl::StrCat(type_and_length.second.value()) |
| : "<unspecified>"}); |
| } |
| return out; |
| } |
| |
| // Custom gMock matcher, used to implement EqualsFrames(). |
| class SpdyControlFrameMatcher |
| : public testing::MatcherInterface<absl::string_view> { |
| public: |
| explicit SpdyControlFrameMatcher( |
| std::vector<TypeAndOptionalLength> types_and_lengths) |
| : expected_types_and_lengths_(std::move(types_and_lengths)) {} |
| |
| bool MatchAndExplain(absl::string_view s, |
| testing::MatchResultListener* listener) const override { |
| spdy::SpdyFrameReader reader(s.data(), s.size()); |
| |
| for (TypeAndOptionalLength expected : expected_types_and_lengths_) { |
| if (!MatchAndExplainOneFrame(expected.first, expected.second, &reader, |
| listener)) { |
| return false; |
| } |
| } |
| if (!reader.IsDoneReading()) { |
| size_t bytes_remaining = s.size() - reader.GetBytesConsumed(); |
| *listener << "; " << bytes_remaining << " bytes left to read!"; |
| return false; |
| } |
| return true; |
| } |
| |
| bool MatchAndExplainOneFrame(spdy::SpdyFrameType expected_type, |
| absl::optional<size_t> expected_length, |
| spdy::SpdyFrameReader* reader, |
| testing::MatchResultListener* listener) const { |
| uint32_t payload_length; |
| if (!reader->ReadUInt24(&payload_length)) { |
| *listener << "; unable to read length field for expected_type " |
| << FrameTypeToString(expected_type) << ". data too short!"; |
| return false; |
| } |
| |
| if (expected_length && payload_length != expected_length.value()) { |
| *listener << "; actual length: " << payload_length |
| << " but expected length: " << expected_length.value(); |
| return false; |
| } |
| |
| uint8_t raw_type; |
| if (!reader->ReadUInt8(&raw_type)) { |
| *listener << "; unable to read type field for expected_type " |
| << FrameTypeToString(expected_type) << ". data too short!"; |
| return false; |
| } |
| |
| if (!spdy::IsDefinedFrameType(raw_type)) { |
| *listener << "; expected type " << FrameTypeToString(expected_type) |
| << " but raw type " << static_cast<int>(raw_type) |
| << " is not a defined frame type!"; |
| return false; |
| } |
| |
| spdy::SpdyFrameType actual_type = spdy::ParseFrameType(raw_type); |
| if (actual_type != expected_type) { |
| *listener << "; actual type: " << FrameTypeToString(actual_type) |
| << " but expected type: " << FrameTypeToString(expected_type); |
| return false; |
| } |
| |
| // Seek past flags (1B), stream ID (4B), and payload. Reach the next frame. |
| reader->Seek(5 + payload_length); |
| return true; |
| } |
| |
| void DescribeTo(std::ostream* os) const override { |
| *os << "Data contains frames of types in sequence " |
| << LogFriendly(expected_types_and_lengths_); |
| } |
| |
| void DescribeNegationTo(std::ostream* os) const override { |
| *os << "Data does not contain frames of types in sequence " |
| << LogFriendly(expected_types_and_lengths_); |
| } |
| |
| private: |
| const std::vector<TypeAndOptionalLength> expected_types_and_lengths_; |
| }; |
| |
| // Custom gMock matcher, used to implement HasFrameHeader(). |
| class FrameHeaderMatcher |
| : public testing::MatcherInterface<const nghttp2_frame_hd*> { |
| public: |
| FrameHeaderMatcher(int32_t streamid, |
| uint8_t type, |
| const testing::Matcher<int> flags) |
| : stream_id_(streamid), type_(type), flags_(flags) {} |
| |
| bool MatchAndExplain(const nghttp2_frame_hd* frame, |
| testing::MatchResultListener* listener) const override { |
| bool matched = true; |
| if (stream_id_ != frame->stream_id) { |
| *listener << "; expected stream " << stream_id_ << ", saw " |
| << frame->stream_id; |
| matched = false; |
| } |
| if (type_ != frame->type) { |
| *listener << "; expected frame type " << type_ << ", saw " |
| << static_cast<int>(frame->type); |
| matched = false; |
| } |
| if (!flags_.MatchAndExplain(frame->flags, listener)) { |
| matched = false; |
| } |
| return matched; |
| } |
| |
| void DescribeTo(std::ostream* os) const override { |
| *os << "contains a frame header with stream " << stream_id_ << ", type " |
| << type_ << ", "; |
| flags_.DescribeTo(os); |
| } |
| |
| void DescribeNegationTo(std::ostream* os) const override { |
| *os << "does not contain a frame header with stream " << stream_id_ |
| << ", type " << type_ << ", "; |
| flags_.DescribeNegationTo(os); |
| } |
| |
| private: |
| const int32_t stream_id_; |
| const int type_; |
| const testing::Matcher<int> flags_; |
| }; |
| |
| class DataMatcher : public testing::MatcherInterface<const nghttp2_frame*> { |
| public: |
| DataMatcher(const testing::Matcher<uint32_t> stream_id, |
| const testing::Matcher<size_t> length, |
| const testing::Matcher<int> flags) |
| : stream_id_(stream_id), length_(length), flags_(flags) {} |
| |
| bool MatchAndExplain(const nghttp2_frame* frame, |
| testing::MatchResultListener* listener) const override { |
| if (frame->hd.type != NGHTTP2_DATA) { |
| *listener << "; expected DATA frame, saw frame of type " |
| << static_cast<int>(frame->hd.type); |
| return false; |
| } |
| bool matched = true; |
| if (!stream_id_.MatchAndExplain(frame->hd.stream_id, listener)) { |
| matched = false; |
| } |
| if (!length_.MatchAndExplain(frame->hd.length, listener)) { |
| matched = false; |
| } |
| if (!flags_.MatchAndExplain(frame->hd.flags, listener)) { |
| matched = false; |
| } |
| return matched; |
| } |
| |
| void DescribeTo(std::ostream* os) const override { |
| *os << "contains a DATA frame, "; |
| stream_id_.DescribeTo(os); |
| length_.DescribeTo(os); |
| flags_.DescribeTo(os); |
| } |
| |
| void DescribeNegationTo(std::ostream* os) const override { |
| *os << "does not contain a DATA frame, "; |
| stream_id_.DescribeNegationTo(os); |
| length_.DescribeNegationTo(os); |
| flags_.DescribeNegationTo(os); |
| } |
| |
| private: |
| const testing::Matcher<uint32_t> stream_id_; |
| const testing::Matcher<size_t> length_; |
| const testing::Matcher<int> flags_; |
| }; |
| |
| class HeadersMatcher : public testing::MatcherInterface<const nghttp2_frame*> { |
| public: |
| HeadersMatcher(const testing::Matcher<uint32_t> stream_id, |
| const testing::Matcher<int> flags, |
| const testing::Matcher<int> category) |
| : stream_id_(stream_id), flags_(flags), category_(category) {} |
| |
| bool MatchAndExplain(const nghttp2_frame* frame, |
| testing::MatchResultListener* listener) const override { |
| if (frame->hd.type != NGHTTP2_HEADERS) { |
| *listener << "; expected HEADERS frame, saw frame of type " |
| << static_cast<int>(frame->hd.type); |
| return false; |
| } |
| bool matched = true; |
| if (!stream_id_.MatchAndExplain(frame->hd.stream_id, listener)) { |
| matched = false; |
| } |
| if (!flags_.MatchAndExplain(frame->hd.flags, listener)) { |
| matched = false; |
| } |
| if (!category_.MatchAndExplain(frame->headers.cat, listener)) { |
| matched = false; |
| } |
| return matched; |
| } |
| |
| void DescribeTo(std::ostream* os) const override { |
| *os << "contains a HEADERS frame, "; |
| stream_id_.DescribeTo(os); |
| flags_.DescribeTo(os); |
| category_.DescribeTo(os); |
| } |
| |
| void DescribeNegationTo(std::ostream* os) const override { |
| *os << "does not contain a HEADERS frame, "; |
| stream_id_.DescribeNegationTo(os); |
| flags_.DescribeNegationTo(os); |
| category_.DescribeNegationTo(os); |
| } |
| |
| private: |
| const testing::Matcher<uint32_t> stream_id_; |
| const testing::Matcher<int> flags_; |
| const testing::Matcher<int> category_; |
| }; |
| |
| class RstStreamMatcher |
| : public testing::MatcherInterface<const nghttp2_frame*> { |
| public: |
| RstStreamMatcher(const testing::Matcher<uint32_t> stream_id, |
| const testing::Matcher<uint32_t> error_code) |
| : stream_id_(stream_id), error_code_(error_code) {} |
| |
| bool MatchAndExplain(const nghttp2_frame* frame, |
| testing::MatchResultListener* listener) const override { |
| if (frame->hd.type != NGHTTP2_RST_STREAM) { |
| *listener << "; expected RST_STREAM frame, saw frame of type " |
| << static_cast<int>(frame->hd.type); |
| return false; |
| } |
| bool matched = true; |
| if (!stream_id_.MatchAndExplain(frame->hd.stream_id, listener)) { |
| matched = false; |
| } |
| if (!error_code_.MatchAndExplain(frame->rst_stream.error_code, listener)) { |
| matched = false; |
| } |
| return matched; |
| } |
| |
| void DescribeTo(std::ostream* os) const override { |
| *os << "contains a RST_STREAM frame, "; |
| stream_id_.DescribeTo(os); |
| error_code_.DescribeTo(os); |
| } |
| |
| void DescribeNegationTo(std::ostream* os) const override { |
| *os << "does not contain a RST_STREAM frame, "; |
| stream_id_.DescribeNegationTo(os); |
| error_code_.DescribeNegationTo(os); |
| } |
| |
| private: |
| const testing::Matcher<uint32_t> stream_id_; |
| const testing::Matcher<uint32_t> error_code_; |
| }; |
| |
| class SettingsMatcher : public testing::MatcherInterface<const nghttp2_frame*> { |
| public: |
| SettingsMatcher(const testing::Matcher<std::vector<Http2Setting>> values) |
| : values_(values) {} |
| |
| bool MatchAndExplain(const nghttp2_frame* frame, |
| testing::MatchResultListener* listener) const override { |
| if (frame->hd.type != NGHTTP2_SETTINGS) { |
| *listener << "; expected SETTINGS frame, saw frame of type " |
| << static_cast<int>(frame->hd.type); |
| return false; |
| } |
| std::vector<Http2Setting> settings; |
| settings.reserve(frame->settings.niv); |
| for (int i = 0; i < frame->settings.niv; ++i) { |
| const auto& p = frame->settings.iv[i]; |
| settings.push_back({static_cast<uint16_t>(p.settings_id), p.value}); |
| } |
| return values_.MatchAndExplain(settings, listener); |
| } |
| |
| void DescribeTo(std::ostream* os) const override { |
| *os << "contains a SETTINGS frame, "; |
| values_.DescribeTo(os); |
| } |
| |
| void DescribeNegationTo(std::ostream* os) const override { |
| *os << "does not contain a SETTINGS frame, "; |
| values_.DescribeNegationTo(os); |
| } |
| |
| private: |
| const testing::Matcher<std::vector<Http2Setting>> values_; |
| }; |
| |
| class PingMatcher : public testing::MatcherInterface<const nghttp2_frame*> { |
| public: |
| PingMatcher(const testing::Matcher<uint64_t> id, bool is_ack) |
| : id_(id), is_ack_(is_ack) {} |
| |
| bool MatchAndExplain(const nghttp2_frame* frame, |
| testing::MatchResultListener* listener) const override { |
| if (frame->hd.type != NGHTTP2_PING) { |
| *listener << "; expected PING frame, saw frame of type " |
| << static_cast<int>(frame->hd.type); |
| return false; |
| } |
| bool matched = true; |
| bool frame_ack = frame->hd.flags & NGHTTP2_FLAG_ACK; |
| if (is_ack_ != frame_ack) { |
| *listener << "; expected is_ack=" << is_ack_ << ", saw " << frame_ack; |
| matched = false; |
| } |
| uint64_t data; |
| std::memcpy(&data, frame->ping.opaque_data, sizeof(data)); |
| data = quiche::QuicheEndian::HostToNet64(data); |
| if (!id_.MatchAndExplain(data, listener)) { |
| matched = false; |
| } |
| return matched; |
| } |
| |
| void DescribeTo(std::ostream* os) const override { |
| *os << "contains a PING frame, "; |
| id_.DescribeTo(os); |
| } |
| |
| void DescribeNegationTo(std::ostream* os) const override { |
| *os << "does not contain a PING frame, "; |
| id_.DescribeNegationTo(os); |
| } |
| |
| private: |
| const testing::Matcher<uint64_t> id_; |
| const bool is_ack_; |
| }; |
| |
| class GoAwayMatcher : public testing::MatcherInterface<const nghttp2_frame*> { |
| public: |
| GoAwayMatcher(const testing::Matcher<uint32_t> last_stream_id, |
| const testing::Matcher<uint32_t> error_code, |
| const testing::Matcher<absl::string_view> opaque_data) |
| : last_stream_id_(last_stream_id), |
| error_code_(error_code), |
| opaque_data_(opaque_data) {} |
| |
| bool MatchAndExplain(const nghttp2_frame* frame, |
| testing::MatchResultListener* listener) const override { |
| if (frame->hd.type != NGHTTP2_GOAWAY) { |
| *listener << "; expected GOAWAY frame, saw frame of type " |
| << static_cast<int>(frame->hd.type); |
| return false; |
| } |
| bool matched = true; |
| if (!last_stream_id_.MatchAndExplain(frame->goaway.last_stream_id, |
| listener)) { |
| matched = false; |
| } |
| if (!error_code_.MatchAndExplain(frame->goaway.error_code, listener)) { |
| matched = false; |
| } |
| auto opaque_data = |
| ToStringView(frame->goaway.opaque_data, frame->goaway.opaque_data_len); |
| if (!opaque_data_.MatchAndExplain(opaque_data, listener)) { |
| matched = false; |
| } |
| return matched; |
| } |
| |
| void DescribeTo(std::ostream* os) const override { |
| *os << "contains a GOAWAY frame, "; |
| last_stream_id_.DescribeTo(os); |
| error_code_.DescribeTo(os); |
| opaque_data_.DescribeTo(os); |
| } |
| |
| void DescribeNegationTo(std::ostream* os) const override { |
| *os << "does not contain a GOAWAY frame, "; |
| last_stream_id_.DescribeNegationTo(os); |
| error_code_.DescribeNegationTo(os); |
| opaque_data_.DescribeNegationTo(os); |
| } |
| |
| private: |
| const testing::Matcher<uint32_t> last_stream_id_; |
| const testing::Matcher<uint32_t> error_code_; |
| const testing::Matcher<absl::string_view> opaque_data_; |
| }; |
| |
| class WindowUpdateMatcher |
| : public testing::MatcherInterface<const nghttp2_frame*> { |
| public: |
| WindowUpdateMatcher(const testing::Matcher<uint32_t> delta) : delta_(delta) {} |
| |
| bool MatchAndExplain(const nghttp2_frame* frame, |
| testing::MatchResultListener* listener) const override { |
| if (frame->hd.type != NGHTTP2_WINDOW_UPDATE) { |
| *listener << "; expected WINDOW_UPDATE frame, saw frame of type " |
| << static_cast<int>(frame->hd.type); |
| return false; |
| } |
| return delta_.MatchAndExplain(frame->window_update.window_size_increment, |
| listener); |
| } |
| |
| void DescribeTo(std::ostream* os) const override { |
| *os << "contains a WINDOW_UPDATE frame, "; |
| delta_.DescribeTo(os); |
| } |
| |
| void DescribeNegationTo(std::ostream* os) const override { |
| *os << "does not contain a WINDOW_UPDATE frame, "; |
| delta_.DescribeNegationTo(os); |
| } |
| |
| private: |
| const testing::Matcher<uint32_t> delta_; |
| }; |
| |
| } // namespace |
| |
| testing::Matcher<absl::string_view> EqualsFrames( |
| std::vector<std::pair<spdy::SpdyFrameType, absl::optional<size_t>>> |
| types_and_lengths) { |
| return MakeMatcher(new SpdyControlFrameMatcher(std::move(types_and_lengths))); |
| } |
| |
| testing::Matcher<absl::string_view> EqualsFrames( |
| std::vector<spdy::SpdyFrameType> types) { |
| std::vector<std::pair<spdy::SpdyFrameType, absl::optional<size_t>>> |
| types_and_lengths; |
| types_and_lengths.reserve(types.size()); |
| for (spdy::SpdyFrameType type : types) { |
| types_and_lengths.push_back({type, absl::nullopt}); |
| } |
| return MakeMatcher(new SpdyControlFrameMatcher(std::move(types_and_lengths))); |
| } |
| |
| testing::Matcher<const nghttp2_frame_hd*> HasFrameHeader( |
| uint32_t streamid, |
| uint8_t type, |
| const testing::Matcher<int> flags) { |
| return MakeMatcher(new FrameHeaderMatcher(streamid, type, flags)); |
| } |
| |
| testing::Matcher<const nghttp2_frame*> IsData( |
| const testing::Matcher<uint32_t> stream_id, |
| const testing::Matcher<size_t> length, |
| const testing::Matcher<int> flags) { |
| return MakeMatcher(new DataMatcher(stream_id, length, flags)); |
| } |
| |
| testing::Matcher<const nghttp2_frame*> IsHeaders( |
| const testing::Matcher<uint32_t> stream_id, |
| const testing::Matcher<int> flags, |
| const testing::Matcher<int> category) { |
| return MakeMatcher(new HeadersMatcher(stream_id, flags, category)); |
| } |
| |
| testing::Matcher<const nghttp2_frame*> IsRstStream( |
| const testing::Matcher<uint32_t> stream_id, |
| const testing::Matcher<uint32_t> error_code) { |
| return MakeMatcher(new RstStreamMatcher(stream_id, error_code)); |
| } |
| |
| testing::Matcher<const nghttp2_frame*> IsSettings( |
| const testing::Matcher<std::vector<Http2Setting>> values) { |
| return MakeMatcher(new SettingsMatcher(values)); |
| } |
| |
| testing::Matcher<const nghttp2_frame*> IsPing( |
| const testing::Matcher<uint64_t> id) { |
| return MakeMatcher(new PingMatcher(id, false)); |
| } |
| |
| testing::Matcher<const nghttp2_frame*> IsPingAck( |
| const testing::Matcher<uint64_t> id) { |
| return MakeMatcher(new PingMatcher(id, true)); |
| } |
| |
| testing::Matcher<const nghttp2_frame*> IsGoAway( |
| const testing::Matcher<uint32_t> last_stream_id, |
| const testing::Matcher<uint32_t> error_code, |
| const testing::Matcher<absl::string_view> opaque_data) { |
| return MakeMatcher( |
| new GoAwayMatcher(last_stream_id, error_code, opaque_data)); |
| } |
| |
| testing::Matcher<const nghttp2_frame*> IsWindowUpdate( |
| const testing::Matcher<uint32_t> delta) { |
| return MakeMatcher(new WindowUpdateMatcher(delta)); |
| } |
| |
| } // namespace test |
| } // namespace adapter |
| } // namespace http2 |