diff options
Diffstat (limited to 'src/lib/http/tests/request_parser_unittests.cc')
-rw-r--r-- | src/lib/http/tests/request_parser_unittests.cc | 328 |
1 files changed, 328 insertions, 0 deletions
diff --git a/src/lib/http/tests/request_parser_unittests.cc b/src/lib/http/tests/request_parser_unittests.cc new file mode 100644 index 0000000000..8860ff8760 --- /dev/null +++ b/src/lib/http/tests/request_parser_unittests.cc @@ -0,0 +1,328 @@ +// Copyright (C) 2016-2017 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> + +#include <cc/data.h> +#include <http/http_types.h> +#include <http/request_parser.h> +#include <http/post_request_json.h> +#include <gtest/gtest.h> +#include <sstream> + +using namespace isc::data; +using namespace isc::http; + +namespace { + +/// @brief Test fixture class for @ref HttpRequestParser. +class HttpRequestParserTest : public ::testing::Test { +public: + + /// @brief Creates HTTP request string. + /// + /// @param preamble A string including HTTP request's first line + /// and all headers except "Content-Length". + /// @param payload A string containing HTTP request payload. + std::string createRequestString(const std::string& preamble, + const std::string& payload) { + std::ostringstream s; + s << preamble; + s << "Content-Length: " << payload.length() << "\r\n\r\n" + << payload; + return (s.str()); + } + + /// @brief Parses the HTTP request and checks that parsing was + /// successful. + /// + /// @param http_req HTTP request string. + void doParse(const std::string& http_req) { + HttpRequestParser parser(request_); + ASSERT_NO_THROW(parser.initModel()); + + parser.postBuffer(&http_req[0], http_req.size()); + ASSERT_NO_THROW(parser.poll()); + + ASSERT_FALSE(parser.needData()); + ASSERT_TRUE(parser.httpParseOk()); + EXPECT_TRUE(parser.getErrorMessage().empty()); + } + + /// @brief Tests that parsing fails when malformed HTTP request + /// is received. + /// + /// @param http_req HTTP request string. + void testInvalidHttpRequest(const std::string& http_req) { + HttpRequestParser parser(request_); + ASSERT_NO_THROW(parser.initModel()); + + parser.postBuffer(&http_req[0], http_req.size()); + ASSERT_NO_THROW(parser.poll()); + + EXPECT_FALSE(parser.needData()); + EXPECT_FALSE(parser.httpParseOk()); + EXPECT_FALSE(parser.getErrorMessage().empty()); + } + + /// @brief Instance of the HttpRequest used by the unit tests. + HttpRequest request_; +}; + +// Test test verifies that an HTTP request including JSON body is parsed +// successfully. +TEST_F(HttpRequestParserTest, postHttpRequestWithJson) { + std::string http_req = "POST /foo/bar HTTP/1.0\r\n" + "Content-Type: application/json\r\n"; + std::string json = "{ \"service\": \"dhcp4\", \"command\": \"shutdown\" }"; + + http_req = createRequestString(http_req, json); + + // Create HTTP request which accepts POST method and JSON as a body. + PostHttpRequestJson request; + + // Create a parser and make it use the request we created. + HttpRequestParser parser(request); + ASSERT_NO_THROW(parser.initModel()); + + // Simulate receiving HTTP request in chunks. + for (size_t i = 0; i < http_req.size(); i += http_req.size() / 10) { + bool done = false; + // Get the size of the data chunk. + size_t chunk = http_req.size() / 10; + // When we're near the end of the data stream, the chunk length may + // vary. + if (i + chunk > http_req.size()) { + chunk = http_req.size() - i; + done = true; + } + // Feed the parser with a data chunk and parse it. + parser.postBuffer(&http_req[i], chunk); + parser.poll(); + if (!done) { + ASSERT_TRUE(parser.needData()); + } + } + + // Parser should have parsed the request and should expect no more data. + ASSERT_FALSE(parser.needData()); + // Parsing should be successful. + ASSERT_TRUE(parser.httpParseOk()); + // There should be no error message. + EXPECT_TRUE(parser.getErrorMessage().empty()); + + // Verify parsed headers etc. + EXPECT_EQ(HttpRequest::Method::HTTP_POST, request.getMethod()); + EXPECT_EQ("/foo/bar", request.getUri()); + EXPECT_EQ("application/json", request.getHeaderValue("Content-Type")); + EXPECT_EQ(json.length(), request.getHeaderValueAsUint64("Content-Length")); + EXPECT_EQ(1, request.getHttpVersion().major_); + EXPECT_EQ(0, request.getHttpVersion().minor_); + + // Try to retrieve values carried in JSON payload. + ConstElementPtr json_element; + ASSERT_NO_THROW(json_element = request.getJsonElement("service")); + EXPECT_EQ("dhcp4", json_element->stringValue()); + + ASSERT_NO_THROW(json_element = request.getJsonElement("command")); + EXPECT_EQ("shutdown", json_element->stringValue()); +} + +// This test verifies that extraneous data in the request will not cause +// an error if "Content-Length" value refers to the length of the valid +// part of the request. +TEST_F(HttpRequestParserTest, extraneousDataInRequest) { + std::string http_req = "POST /foo/bar HTTP/1.0\r\n" + "Content-Type: application/json\r\n"; + std::string json = "{ \"service\": \"dhcp4\", \"command\": \"shutdown\" }"; + + // Create valid request; + http_req = createRequestString(http_req, json); + + // Add some garbage at the end. + http_req += "some stuff which, if parsed, will cause errors"; + + // Create HTTP request which accepts POST method and JSON as a body. + PostHttpRequestJson request; + + // Create a parser and make it use the request we created. + HttpRequestParser parser(request); + ASSERT_NO_THROW(parser.initModel()); + + // Feed the parser with the request containing some garbage at the end. + parser.postBuffer(&http_req[0], http_req.size()); + ASSERT_NO_THROW(parser.poll()); + + // The parser should only parse the valid part of the request as indicated + // by the Content-Length. + ASSERT_FALSE(parser.needData()); + ASSERT_TRUE(parser.httpParseOk()); + // There should be no error message. + EXPECT_TRUE(parser.getErrorMessage().empty()); + + // Do another poll() to see if the parser will parse the garbage. We + // expect that it doesn't. + ASSERT_NO_THROW(parser.poll()); + EXPECT_FALSE(parser.needData()); + EXPECT_TRUE(parser.httpParseOk()); + EXPECT_TRUE(parser.getErrorMessage().empty()); +} + + +// This test verifies that LWS is parsed correctly. The LWS marks line breaks +// in the HTTP header values. +TEST_F(HttpRequestParserTest, getLWS) { + // "User-Agent" header contains line breaks with whitespaces in the new + // lines to mark continuation of the header value. + std::string http_req = "GET /foo/bar HTTP/1.1\r\n" + "Content-Type: text/html\r\n" + "User-Agent: Kea/1.2 Command \r\n" + " Control \r\n" + "\tClient\r\n\r\n"; + + ASSERT_NO_FATAL_FAILURE(doParse(http_req)); + + // Verify parsed values. + EXPECT_EQ(HttpRequest::Method::HTTP_GET, request_.getMethod()); + EXPECT_EQ("/foo/bar", request_.getUri()); + EXPECT_EQ("text/html", request_.getHeaderValue("Content-Type")); + EXPECT_EQ("Kea/1.2 Command Control Client", + request_.getHeaderValue("User-Agent")); + EXPECT_EQ(1, request_.getHttpVersion().major_); + EXPECT_EQ(1, request_.getHttpVersion().minor_); +} + +// This test verifies that the HTTP request with no headers is +// parsed correctly. +TEST_F(HttpRequestParserTest, noHeaders) { + std::string http_req = "GET /foo/bar HTTP/1.1\r\n\r\n"; + + ASSERT_NO_FATAL_FAILURE(doParse(http_req)); + + // Verify the values. + EXPECT_EQ(HttpRequest::Method::HTTP_GET, request_.getMethod()); + EXPECT_EQ("/foo/bar", request_.getUri()); + EXPECT_EQ(1, request_.getHttpVersion().major_); + EXPECT_EQ(1, request_.getHttpVersion().minor_); +} + +// This test verifies that the HTTP method can be specified in lower +// case. +TEST_F(HttpRequestParserTest, getLowerCase) { + std::string http_req = "get /foo/bar HTTP/1.1\r\n" + "Content-Type: text/html\r\n\r\n"; + + ASSERT_NO_FATAL_FAILURE(doParse(http_req)); + + EXPECT_EQ(HttpRequest::Method::HTTP_GET, request_.getMethod()); + EXPECT_EQ("/foo/bar", request_.getUri()); + EXPECT_EQ("text/html", request_.getHeaderValue("Content-Type")); + EXPECT_EQ(1, request_.getHttpVersion().major_); + EXPECT_EQ(1, request_.getHttpVersion().minor_); +} + +// This test verifies that other value of the HTTP version can be +// specified in the request. +TEST_F(HttpRequestParserTest, http20) { + std::string http_req = "get /foo/bar HTTP/2.0\r\n" + "Content-Type: text/html\r\n\r\n"; + + ASSERT_NO_FATAL_FAILURE(doParse(http_req)); + + EXPECT_EQ(HttpRequest::Method::HTTP_GET, request_.getMethod()); + EXPECT_EQ("/foo/bar", request_.getUri()); + EXPECT_EQ("text/html", request_.getHeaderValue("Content-Type")); + EXPECT_EQ(2, request_.getHttpVersion().major_); + EXPECT_EQ(0, request_.getHttpVersion().minor_); +} + +// This test verifies that the header with no whitespace between the +// colon and header value is accepted. +TEST_F(HttpRequestParserTest, noHeaderWhitespace) { + std::string http_req = "get /foo/bar HTTP/1.0\r\n" + "Content-Type:text/html\r\n\r\n"; + + ASSERT_NO_FATAL_FAILURE(doParse(http_req)); + + EXPECT_EQ(HttpRequest::Method::HTTP_GET, request_.getMethod()); + EXPECT_EQ("/foo/bar", request_.getUri()); + EXPECT_EQ("text/html", request_.getHeaderValue("Content-Type")); + EXPECT_EQ(1, request_.getHttpVersion().major_); + EXPECT_EQ(0, request_.getHttpVersion().minor_); +} + +// This test verifies that the header value preceded with multiple +// whitespaces is accepted. +TEST_F(HttpRequestParserTest, multipleLeadingHeaderWhitespaces) { + std::string http_req = "get /foo/bar HTTP/1.0\r\n" + "Content-Type: text/html\r\n\r\n"; + + ASSERT_NO_FATAL_FAILURE(doParse(http_req)); + + EXPECT_EQ(HttpRequest::Method::HTTP_GET, request_.getMethod()); + EXPECT_EQ("/foo/bar", request_.getUri()); + EXPECT_EQ("text/html", request_.getHeaderValue("Content-Type")); + EXPECT_EQ(1, request_.getHttpVersion().major_); + EXPECT_EQ(0, request_.getHttpVersion().minor_); +} + +// This test verifies that error is reported when unsupported HTTP +// method is used. +TEST_F(HttpRequestParserTest, unsupportedMethod) { + std::string http_req = "POSTX /foo/bar HTTP/2.0\r\n" + "Content-Type: text/html\r\n\r\n"; + testInvalidHttpRequest(http_req); +} + +// This test verifies that error is reported when URI contains +// an invalid character. +TEST_F(HttpRequestParserTest, invalidUri) { + std::string http_req = "POST /foo/\r HTTP/2.0\r\n" + "Content-Type: text/html\r\n\r\n"; + testInvalidHttpRequest(http_req); +} + +// This test verifies that the request containing a typo in the +// HTTP version string causes parsing error. +TEST_F(HttpRequestParserTest, invalidHTTPString) { + std::string http_req = "POST /foo/ HTLP/2.0\r\n" + "Content-Type: text/html\r\n\r\n"; + testInvalidHttpRequest(http_req); +} + +// This test verifies that error is reported when the HTTP version +// string doesn't contain a slash character. +TEST_F(HttpRequestParserTest, invalidHttpVersionNoSlash) { + std::string http_req = "POST /foo/ HTTP 1.1\r\n" + "Content-Type: text/html\r\n\r\n"; + testInvalidHttpRequest(http_req); +} + +// This test verifies that error is reported when HTTP version string +// doesn't contain the minor version number. +TEST_F(HttpRequestParserTest, invalidHttpNoMinorVersion) { + std::string http_req = "POST /foo/ HTTP/1\r\n" + "Content-Type: text/html\r\n\r\n"; + testInvalidHttpRequest(http_req); +} + +// This test verifies that error is reported when HTTP header name +// contains an invalid character. +TEST_F(HttpRequestParserTest, invalidHeaderName) { + std::string http_req = "POST /foo/ HTTP/1.1\r\n" + "Content-;: text/html\r\n\r\n"; + testInvalidHttpRequest(http_req); +} + +// This test verifies that error is reported when HTTP header value +// is not preceded with the colon character. +TEST_F(HttpRequestParserTest, noColonInHttpHeader) { + std::string http_req = "POST /foo/ HTTP/1.1\r\n" + "Content-Type text/html\r\n\r\n"; + testInvalidHttpRequest(http_req); +} + +} |