avs-device-sdk/ACL/test/Transport/MimeParserFuzzTest.cpp

268 lines
10 KiB
C++

/*
* 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 <memory>
#include <random>
#include <thread>
#include <curl/curl.h>
#include <gtest/gtest.h>
#include "ACL/Transport/MessageConsumerInterface.h"
#include "ACL/Transport/HTTP2Stream.h"
#include <AVSCommon/AVS/Attachment/AttachmentReader.h>
#include <AVSCommon/AVS/Attachment/AttachmentWriter.h>
#include "AVSCommon/AVS/Attachment/InProcessAttachment.h"
#include <AVSCommon/SDKInterfaces/MessageObserverInterface.h>
#include <AVSCommon/Utils/Logger/Logger.h>
#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<uint8_t>* buffer, size_t size);
private:
/// The underlying random byte generator.
std::independent_bits_engine<std::minstd_rand, CHAR_BIT, uint8_t> m_engine;
};
ByteGenerator::ByteGenerator() {
m_engine.seed(BYTES_SEED);
}
void ByteGenerator::generateBytes(std::vector<uint8_t>* 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<AttachmentManager> m_attachmentManager;
/// The MimeParser which we will be primarily testing.
std::shared_ptr<MimeParser> m_parser;
/// Flag to indicate the test has failed and loops should exit.
std::atomic<bool> m_failed;
};
void MimeParserFuzzTest::SetUp() {
m_attachmentManager = std::make_shared<AttachmentManager>(AttachmentManager::AttachmentType::IN_PROCESS);
auto testableConsumer = std::make_shared<TestableConsumer>();
testableConsumer->setMessageObserver(std::make_shared<TestableMessageObserver>());
m_parser = std::make_shared<MimeParser>(testableConsumer, m_attachmentManager);
m_parser->setAttachmentContextId(CONTEXT_ID);
m_parser->setBoundaryString(BOUNDARY);
m_failed = false;
}
void MimeParserFuzzTest::feed() {
m_parser->feed(const_cast<char*>(ATTACHMENT_PREFIX.c_str()), ATTACHMENT_PREFIX.size());
ByteGenerator feedBytesSource;
std::default_random_engine feedSizeGenerator;
feedSizeGenerator.seed(FEED_SIZE_SEED);
std::uniform_int_distribution<int> feedSizeDistribution(1, MAX_FEED_SIZE);
auto feedSizeSource = std::bind(feedSizeDistribution, feedSizeGenerator);
std::vector<uint8_t> 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<char*>(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<int> 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<uint8_t> readBuffer(MAX_READ_SIZE);
std::vector<uint8_t> 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