/* * Copyright 2017-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0/ * * or in the "license" file accompanying this file. This file is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing * permissions and limitations under the License. */ /// @file MimeParserFuzzTest.cpp #include #include #include #include #include #include "ACL/Transport/MessageConsumerInterface.h" #include "ACL/Transport/HTTP2Stream.h" #include #include #include "AVSCommon/AVS/Attachment/InProcessAttachment.h" #include #include #include "Common/TestableAttachmentManager.h" #include "Common/Common.h" #include "Common/MimeUtils.h" #include "Common/TestableMessageObserver.h" #include "MockMessageRequest.h" #include "TestableConsumer.h" namespace alexaClientSDK { namespace acl { namespace test { using namespace avsCommon::sdkInterfaces; using namespace avsCommon::avs::attachment; /// String to identify log entries originating from this file. static const std::string TAG("MimeParserFuzzTest"); /** * Create a LogEntry using this file's TAG and the specified event string. * * @param The event string for this @c LogEntry. */ #define LX(event) alexaClientSDK::avsCommon::utils::logger::LogEntry(TAG, event) /// The number of feed() calls to be made in @c MimeParserFuzzTest::feed() static const int FEED_COUNT = 0x100; /// Max pseudo-random buffer size to pass to @c feed() (large enough to assure we trigger attachment buffer full) static const size_t MAX_FEED_SIZE = (InProcessAttachment::SDS_BUFFER_DEFAULT_SIZE_IN_BYTES / FEED_COUNT) * 4; /// Max buffer size to read from the attachment reader (small enough trigger multiple reads per feed) static const size_t MAX_READ_SIZE = MAX_FEED_SIZE / 8; /// Consistent seed (for repeatable tests) with which to generate pseudo-random bytes. static const unsigned int BYTES_SEED = 1; /// Consistent seed (for repeatable tests) with which to generate pseudo-random feed sizes. static const unsigned int FEED_SIZE_SEED = 2; /// Consistent but different seed (for repeatable tests) with which to generate pseudo-random read sizes. static const unsigned int READ_SIZE_SEED = FEED_SIZE_SEED + 1; /// Content ID for mime part. static const std::string CONTENT_ID = "CONTENT_ID"; /// Context ID for generating attachment ID. static const std::string CONTEXT_ID = "CONTEXT_ID"; /// CR,LF to separate lines in mime headers and boundaries. static const std::string CRLF = "\r\n"; /// Dashes used as a prefix or suffix to mime boundaries. static const std::string DASHES = "--"; /// Mime Boundary string (without CR,LF or dashes). static const std::string BOUNDARY = "BoundaryBoundaryBoundaryBoundaryBoundaryBoundaryBoundary"; /// Mime prefix to our attachment. static const std::string ATTACHMENT_PREFIX = CRLF + DASHES + BOUNDARY + CRLF + "Content-ID: " + CONTENT_ID + CRLF + "Content-Type: application/octet-stream" + CRLF + CRLF; /// Mime suffix terminating our attachment. static const std::string ATTACHMENT_SUFFIX = CRLF + DASHES + BOUNDARY + DASHES; /** * Class to generate consistent pseudo-random byte stream. */ class ByteGenerator { public: ByteGenerator(); /** * Generate the next set of bytes in the stream. * * @param[out] buffer The buffer in which to place the generated bytes. * @param size The number of bytes to generate. */ void generateBytes(std::vector* buffer, size_t size); private: /// The underlying random byte generator. std::independent_bits_engine m_engine; }; ByteGenerator::ByteGenerator() { m_engine.seed(BYTES_SEED); } void ByteGenerator::generateBytes(std::vector* buffer, size_t size) { for (size_t ix = 0; ix < size; ix++) { (*buffer)[ix] = m_engine(); } } /** * Our GTest class. */ class MimeParserFuzzTest : public ::testing::Test { public: /** * Construct the objects we will use across tests. */ void SetUp() override; /** * Feed a pseudo-random stream of bytes to the mime parser, making a number of * calls with pseudo-random sizes. */ void feed(); /** * Read the attachment the mime parser is rendering to, requesting a pseudo-random * number of bytes with each read. */ void read(); /// The AttachmentManager. std::shared_ptr m_attachmentManager; /// The MimeParser which we will be primarily testing. std::shared_ptr m_parser; /// Flag to indicate the test has failed and loops should exit. std::atomic m_failed; }; void MimeParserFuzzTest::SetUp() { m_attachmentManager = std::make_shared(AttachmentManager::AttachmentType::IN_PROCESS); auto testableConsumer = std::make_shared(); testableConsumer->setMessageObserver(std::make_shared()); m_parser = std::make_shared(testableConsumer, m_attachmentManager); m_parser->setAttachmentContextId(CONTEXT_ID); m_parser->setBoundaryString(BOUNDARY); m_failed = false; } void MimeParserFuzzTest::feed() { m_parser->feed(const_cast(ATTACHMENT_PREFIX.c_str()), ATTACHMENT_PREFIX.size()); ByteGenerator feedBytesSource; std::default_random_engine feedSizeGenerator; feedSizeGenerator.seed(FEED_SIZE_SEED); std::uniform_int_distribution feedSizeDistribution(1, MAX_FEED_SIZE); auto feedSizeSource = std::bind(feedSizeDistribution, feedSizeGenerator); std::vector feedBuffer(MAX_FEED_SIZE); size_t totalBytesFed = 0; int incompleteCount = 0; for (int ix = 0; !m_failed && ix < FEED_COUNT; ix++) { auto feedSize = feedSizeSource(); feedBytesSource.generateBytes(&feedBuffer, feedSize); while (true) { ACSDK_DEBUG9(LX("callingFeed").d("totalBytesFed", totalBytesFed).d("feedSize", feedSize)); auto status = m_parser->feed((char*)(&feedBuffer[0]), feedSize); if (MimeParser::DataParsedStatus::OK == status) { totalBytesFed += feedSize; break; } else if (MimeParser::DataParsedStatus::INCOMPLETE == status) { ACSDK_DEBUG9(LX("feedIncomplete").d("action", "waitAndReDrive")); ++incompleteCount; std::this_thread::sleep_for(std::chrono::milliseconds(1)); } else { ASSERT_NE(status, status); } } } m_parser->feed(const_cast(ATTACHMENT_SUFFIX.c_str()), ATTACHMENT_SUFFIX.size()); // Make sure there were enough INCOMPLETE return statuses. ACSDK_DEBUG9(LX("doneFeeding").d("incompleteCount", incompleteCount).d("FEED_COUNT", FEED_COUNT)); ASSERT_GT(incompleteCount, FEED_COUNT); } void MimeParserFuzzTest::read() { ByteGenerator verifyBytesSource; std::default_random_engine readSizeGenerator; readSizeGenerator.seed(READ_SIZE_SEED); std::uniform_int_distribution readSizeDistribution(1, MAX_READ_SIZE); auto readSizeSource = std::bind(readSizeDistribution, readSizeGenerator); auto attachmentId = m_attachmentManager->generateAttachmentId(CONTEXT_ID, CONTENT_ID); auto reader = m_attachmentManager->createReader(attachmentId, avsCommon::utils::sds::ReaderPolicy::BLOCKING); std::vector readBuffer(MAX_READ_SIZE); std::vector verifyBuffer(MAX_READ_SIZE); size_t totalBytesRead = 0; while (true) { // Delay reads so that AnotherMimeParserTest::feed() blocks on a full attachment buffer // in a reliable way. This makes the test results consistent run to run. std::this_thread::sleep_for(std::chrono::milliseconds(5)); auto readSizeIn = readSizeSource(); auto readStatus = AttachmentReader::ReadStatus::OK; ACSDK_DEBUG9(LX("callingRead").d("totalBytesRead", totalBytesRead).d("readSizeIn", readSizeIn)); auto readSizeOut = reader->read(readBuffer.data(), readSizeIn, &readStatus); if (readSizeOut != 0) { ACSDK_DEBUG9(LX("readReturned").d("readSizeOut", readSizeOut)); verifyBytesSource.generateBytes(&verifyBuffer, readSizeOut); for (size_t ix = 0; ix < readSizeOut; ix++) { auto good = readBuffer[ix] == verifyBuffer[ix]; if (!good) { m_failed = true; ASSERT_EQ(readBuffer[ix], verifyBuffer[ix]) << "UnexpectedByte at: " << totalBytesRead + ix; } } totalBytesRead += readSizeOut; } switch (readStatus) { case AttachmentReader::ReadStatus::OK: break; case AttachmentReader::ReadStatus::CLOSED: ACSDK_DEBUG9(LX("readClosed")); return; case AttachmentReader::ReadStatus::OK_WOULDBLOCK: case AttachmentReader::ReadStatus::OK_TIMEDOUT: case AttachmentReader::ReadStatus::ERROR_OVERRUN: case AttachmentReader::ReadStatus::ERROR_BYTES_LESS_THAN_WORD_SIZE: case AttachmentReader::ReadStatus::ERROR_INTERNAL: ASSERT_NE(readStatus, readStatus); break; } } } /** * Exercise MimeParser's re-drive, parsing, and attachment buffering by feeding in a large attachment * consisting of pseudo-random bytes sent in pseudo-random sized chunks. At the same time, read the * resulting attachment in pseudo-random sized requests. Expect that the data read matches the data * that was fed. */ TEST_F(MimeParserFuzzTest, testRandomFeedAndReadSizesOfRandomData) { auto readThread = std::thread(&MimeParserFuzzTest::read, this); feed(); readThread.join(); } } // namespace test } // namespace acl } // namespace alexaClientSDK