diff options
author | Marcin Siodelski <marcin@isc.org> | 2016-12-08 14:15:28 +0100 |
---|---|---|
committer | Marcin Siodelski <marcin@isc.org> | 2016-12-13 13:52:08 +0100 |
commit | 0fc1359eba5c653c9ec5d4ad3d2940741287b994 (patch) | |
tree | 9b54850dda9601c1652deb9caa94648b50f21585 /src | |
parent | [5077] Created libkea-http library. (diff) | |
download | kea-0fc1359eba5c653c9ec5d4ad3d2940741287b994.tar.xz kea-0fc1359eba5c653c9ec5d4ad3d2940741287b994.zip |
[5077] Implemented HTTP request parser.
Diffstat (limited to 'src')
-rw-r--r-- | src/lib/http/Makefile.am | 7 | ||||
-rw-r--r-- | src/lib/http/header_context.h | 23 | ||||
-rw-r--r-- | src/lib/http/http_log.cc | 2 | ||||
-rw-r--r-- | src/lib/http/post_request.cc | 20 | ||||
-rw-r--r-- | src/lib/http/post_request.h | 31 | ||||
-rw-r--r-- | src/lib/http/post_request_json.cc | 75 | ||||
-rw-r--r-- | src/lib/http/post_request_json.h | 76 | ||||
-rw-r--r-- | src/lib/http/request.cc | 259 | ||||
-rw-r--r-- | src/lib/http/request.h | 277 | ||||
-rw-r--r-- | src/lib/http/request_context.h | 44 | ||||
-rw-r--r-- | src/lib/http/request_parser.cc | 677 | ||||
-rw-r--r-- | src/lib/http/request_parser.h | 450 | ||||
-rw-r--r-- | src/lib/http/tests/Makefile.am | 7 | ||||
-rw-r--r-- | src/lib/http/tests/post_request_json_unittests.cc | 172 | ||||
-rw-r--r-- | src/lib/http/tests/post_request_unittests.cc | 72 | ||||
-rw-r--r-- | src/lib/http/tests/request_parser_unittests.cc | 286 | ||||
-rw-r--r-- | src/lib/http/tests/request_test.h | 83 | ||||
-rw-r--r-- | src/lib/http/tests/request_unittests.cc | 157 |
18 files changed, 2716 insertions, 2 deletions
diff --git a/src/lib/http/Makefile.am b/src/lib/http/Makefile.am index a0cba70b3e..fbbc10afb8 100644 --- a/src/lib/http/Makefile.am +++ b/src/lib/http/Makefile.am @@ -23,6 +23,12 @@ CLEANFILES = *.gcno *.gcda http_messages.h http_messages.cc s-messages lib_LTLIBRARIES = libkea-http.la libkea_http_la_SOURCES = http_log.cc http_log.h +libkea_http_la_SOURCES += header_context.h +libkea_http_la_SOURCES += post_request.cc post_request.h +libkea_http_la_SOURCES += post_request_json.cc post_request_json.h +libkea_http_la_SOURCES += request.cc request.h +libkea_http_la_SOURCES += request_context.h +libkea_http_la_SOURCES += request_parser.cc request_parser.h nodist_libkea_http_la_SOURCES = http_messages.cc http_messages.h @@ -34,6 +40,7 @@ libkea_http_la_LDFLAGS += -no-undefined -version-info 1:0:0 libkea_http_la_LIBADD = libkea_http_la_LIBADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la libkea_http_la_LIBADD += $(top_builddir)/src/lib/log/libkea-log.la +libkea_http_la_LIBADD += $(top_builddir)/src/lib/cc/libkea-cc.la libkea_http_la_LIBADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la libkea_http_la_LIBADD += $(LOG4CPLUS_LIBS) $(BOOST_LIBS) diff --git a/src/lib/http/header_context.h b/src/lib/http/header_context.h new file mode 100644 index 0000000000..125591186e --- /dev/null +++ b/src/lib/http/header_context.h @@ -0,0 +1,23 @@ +// Copyright (C) 2016 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/. + +#ifndef HTTP_HEADER_CONTEXT_H +#define HTTP_HEADER_CONTEXT_H + +#include <string> + +namespace isc { +namespace http { + +struct HttpHeaderContext { + std::string name_; + std::string value_; +}; + +} // namespace http +} // namespace isc + +#endif diff --git a/src/lib/http/http_log.cc b/src/lib/http/http_log.cc index 63f245b96d..64892f26f8 100644 --- a/src/lib/http/http_log.cc +++ b/src/lib/http/http_log.cc @@ -9,7 +9,7 @@ #include <http/http_log.h> namespace isc { -namespace process { +namespace http { /// @brief Defines the logger used within libkea-http library. isc::log::Logger http_logger("http"); diff --git a/src/lib/http/post_request.cc b/src/lib/http/post_request.cc new file mode 100644 index 0000000000..94caca1b26 --- /dev/null +++ b/src/lib/http/post_request.cc @@ -0,0 +1,20 @@ +// Copyright (C) 2016 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 <http/post_request.h> + +namespace isc { +namespace http { + +PostHttpRequest::PostHttpRequest() + : HttpRequest() { + requireHttpMethod(Method::HTTP_POST); + requireHeader("Content-Length"); + requireHeader("Content-Type"); +} + +} // namespace http +} // namespace isc diff --git a/src/lib/http/post_request.h b/src/lib/http/post_request.h new file mode 100644 index 0000000000..04f5564a65 --- /dev/null +++ b/src/lib/http/post_request.h @@ -0,0 +1,31 @@ +// Copyright (C) 2016 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/. + +#ifndef HTTP_POST_REQUEST_H +#define HTTP_POST_REQUEST_H + +#include <http/request.h> + +namespace isc { +namespace http { + +/// @brief Represents HTTP POST request. +/// +/// Instructs the parent class to require: +/// - HTTP POST message type, +/// - Content-Length header, +/// - Content-Type header. +class PostHttpRequest : public HttpRequest { +public: + + /// @brief Constructor. + PostHttpRequest(); +}; + +} // namespace http +} // namespace isc + +#endif diff --git a/src/lib/http/post_request_json.cc b/src/lib/http/post_request_json.cc new file mode 100644 index 0000000000..c12d8be809 --- /dev/null +++ b/src/lib/http/post_request_json.cc @@ -0,0 +1,75 @@ +// Copyright (C) 2016 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 <http/post_request_json.h> + +using namespace isc::data; + +namespace isc { +namespace http { + +PostHttpRequestJson::PostHttpRequestJson() + : PostHttpRequest(), json_() { + requireHeaderValue("Content-Type", "application/json"); +} + +void +PostHttpRequestJson::finalize() { + if (!created_) { + create(); + } + + // Parse JSON body and store. + parseBodyAsJson(); + finalized_ = true; +} + +void +PostHttpRequestJson::reset() { + PostHttpRequest::reset(); + json_.reset(); +} + +ConstElementPtr +PostHttpRequestJson::getBodyAsJson() { + checkFinalized(); + return (json_); +} + +ConstElementPtr +PostHttpRequestJson::getJsonElement(const std::string& element_name) { + try { + ConstElementPtr body = getBodyAsJson(); + if (body) { + const std::map<std::string, ConstElementPtr>& map_value = body->mapValue(); + auto map_element = map_value.find(element_name); + if (map_element != map_value.end()) { + return (map_element->second); + } + } + + } catch (const std::exception& ex) { + isc_throw(HttpRequestJsonError, "unable to get JSON element " + << element_name << ": " << ex.what()); + } + return (ConstElementPtr()); +} + +void +PostHttpRequestJson::parseBodyAsJson() { + try { + // Only parse the body if it hasn't been parsed yet. + if (!json_ && !context_->body_.empty()) { + json_ = Element::fromJSON(context_->body_); + } + } catch (const std::exception& ex) { + isc_throw(HttpRequestJsonError, "unable to parse the body of the HTTP" + " request: " << ex.what()); + } +} + +} // namespace http +} // namespace isc diff --git a/src/lib/http/post_request_json.h b/src/lib/http/post_request_json.h new file mode 100644 index 0000000000..d0f2baec82 --- /dev/null +++ b/src/lib/http/post_request_json.h @@ -0,0 +1,76 @@ +// Copyright (C) 2016 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/. + +#ifndef HTTP_POST_REQUEST_JSON_H +#define HTTP_POST_REQUEST_JSON_H + +#include <cc/data.h> +#include <exceptions/exceptions.h> +#include <http/post_request.h> +#include <string> + +namespace isc { +namespace http { + +/// @brief Exception thrown when body of the HTTP message is not JSON. +class HttpRequestJsonError : public HttpRequestError { +public: + HttpRequestJsonError(const char* file, size_t line, + const char* what) : + HttpRequestError(file, line, what) { }; +}; + +/// @brief Represents HTTP POST request with JSON body. +/// +/// In addition to the requirements specified by the @ref PostHttpRequest +/// this class requires that the "Content-Type" is "application/json". +/// +/// This class provides methods to parse and retrieve JSON data structures. +class PostHttpRequestJson : public PostHttpRequest { +public: + + /// @brief Constructor. + PostHttpRequestJson(); + + /// @brief Complete parsing of the HTTP request. + /// + /// This method parses the JSON body into the structure of + /// @ref data::ConstElementPtr objects. + virtual void finalize(); + + /// @brief Reset the state of the object. + virtual void reset(); + + /// @brief Retrieves JSON body. + /// + /// @return Pointer to the root element of the JSON structure. + /// @throw HttpRequestJsonError if an error occurred. + data::ConstElementPtr getBodyAsJson(); + + /// @brief Retrieves a single JSON element. + /// + /// The element must be at top level of the JSON structure. + /// + /// @param element_name Element name. + /// + /// @return Pointer to the specified element or NULL if such element + /// doesn't exist. + /// @throw HttpRequestJsonError if an error occurred. + data::ConstElementPtr getJsonElement(const std::string& element_name); + +protected: + + void parseBodyAsJson(); + + /// @brief Pointer to the parsed JSON body. + data::ConstElementPtr json_; + +}; + +} // namespace http +} // namespace isc + +#endif diff --git a/src/lib/http/request.cc b/src/lib/http/request.cc new file mode 100644 index 0000000000..55ebbe0839 --- /dev/null +++ b/src/lib/http/request.cc @@ -0,0 +1,259 @@ +// Copyright (C) 2016 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 <http/request.h> +#include <boost/algorithm/string.hpp> +#include <boost/lexical_cast.hpp> + +namespace isc { +namespace http { + +HttpRequest::HttpRequest() + : required_methods_(),required_versions_(), required_headers_(), + created_(false), finalized_(false), method_(Method::HTTP_METHOD_UNKNOWN), + headers_(), context_(new HttpRequestContext()) { +} + +HttpRequest::~HttpRequest() { +} + +void +HttpRequest::requireHttpMethod(const HttpRequest::Method& method) { + required_methods_.insert(method); +} + +void +HttpRequest::requireHttpVersion(const HttpVersion& version) { + required_versions_.insert(version); +} + +void +HttpRequest::requireHeader(const std::string& header_name) { + // Empty value denotes that the header is required but no specific + // value is expected. + required_headers_[header_name] = ""; +} + +void +HttpRequest::requireHeaderValue(const std::string& header_name, + const std::string& header_value) { + required_headers_[header_name] = header_value; +} + +bool +HttpRequest::requiresBody() const { + // If Content-Length is required the body must exist too. There may + // be probably some cases when Content-Length is not provided but + // the body is provided. But, probably not in our use cases. + return (required_headers_.find("Content-Length") != required_headers_.end()); +} + +void +HttpRequest::create() { + try { + // The RequestParser doesn't validate the method name. Thus, this + // may throw an exception. But, we're fine with lower case names, + // e.g. get, post etc. + method_ = methodFromString(context_->method_); + + // Check if the method is allowed for this request. + if (!inRequiredSet(method_, required_methods_)) { + isc_throw(BadValue, "use of HTTP " << methodToString(method_) + << " not allowed"); + } + + // Check if the HTTP version is allowed for this request. + if (!inRequiredSet(std::make_pair(context_->http_version_major_, + context_->http_version_minor_), + required_versions_)) { + isc_throw(BadValue, "use of HTTP version " + << context_->http_version_major_ << "." + << context_->http_version_minor_ + << " not allowed"); + } + + // Copy headers from the context. + for (auto header = context_->headers_.begin(); + header != context_->headers_.end(); + ++header) { + headers_[header->name_] = header->value_; + } + + // Iterate over required headers and check that they exist + // in the HTTP request. + for (auto req_header = required_headers_.begin(); + req_header != required_headers_.end(); + ++req_header) { + auto header = headers_.find(req_header->first); + if (header == headers_.end()) { + isc_throw(BadValue, "required header " << header->first + << " not found in the HTTP request"); + } else if (!req_header->second.empty() && + header->second != req_header->second) { + // If specific value is required for the header, check + // that the value in the HTTP request matches it. + isc_throw(BadValue, "required header's " << header->first + << " value is " << req_header->second + << ", but " << header->second << " was found"); + } + } + + } catch (const std::exception& ex) { + // Reset the state of the object if we failed at any point. + reset(); + isc_throw(HttpRequestError, ex.what()); + } + + // All ok. + created_ = true; +} + +void +HttpRequest::finalize() { + if (!created_) { + create(); + } + + // In this specific case, we don't need to do anything because the + // body is retrieved from the context object directly. We also don't + // know what type of body we have received. Derived classes should + // override this method and handle various types of bodies. + finalized_ = true; +} + +void +HttpRequest::reset() { + created_ = false; + finalized_ = false; + method_ = HttpRequest::Method::HTTP_METHOD_UNKNOWN; + headers_.clear(); +} + +HttpRequest::Method +HttpRequest::getMethod() const { + checkCreated(); + return (method_); +} + +std::string +HttpRequest::getUri() const { + checkCreated(); + return (context_->uri_); +} + +HttpRequest::HttpVersion +HttpRequest::getHttpVersion() const { + checkCreated(); + return (std::make_pair(context_->http_version_major_, + context_->http_version_minor_)); +} + +std::string +HttpRequest::getHeaderValue(const std::string& header) const { + checkCreated(); + + auto header_it = headers_.find(header); + if (header_it != headers_.end()) { + return (header_it->second); + } + // No such header. + isc_throw(HttpRequestNonExistingHeader, header << " HTTP header" + " not found in the request"); +} + +uint64_t +HttpRequest::getHeaderValueAsUint64(const std::string& header) const { + // This will throw an exception if the header doesn't exist. + std::string header_value = getHeaderValue(header); + + try { + return (boost::lexical_cast<uint64_t>(header_value)); + + } catch (const boost::bad_lexical_cast& ex) { + // The specified header does exist, but the value is not a number. + isc_throw(HttpRequestError, header << " HTTP header value " + << header_value << " is not a valid number"); + } +} + +std::string +HttpRequest::getBody() const { + checkFinalized(); + return (context_->body_); +} + +void +HttpRequest::checkCreated() const { + if (!created_) { + isc_throw(HttpRequestError, "unable to retrieve values of HTTP" + " request because the HttpRequest::create() must be" + " called first. This is a programmatic error"); + } +} + +void +HttpRequest::checkFinalized() const { + if (!finalized_) { + isc_throw(HttpRequestError, "unable to retrieve body of HTTP" + " request because the HttpRequest::finalize() must be" + " called first. This is a programmatic error"); + } +} + +template<typename T> +bool +HttpRequest::inRequiredSet(const T& element, + const std::set<T>& element_set) const { + return (element_set.empty() || element_set.count(element) > 0); +} + + +HttpRequest::Method +HttpRequest::methodFromString(std::string method) const { + boost::to_upper(method); + if (method == "GET") { + return (Method::HTTP_GET); + } else if (method == "POST") { + return (Method::HTTP_POST); + } else if (method == "HEAD") { + return (Method::HTTP_HEAD); + } else if (method == "PUT") { + return (Method::HTTP_PUT); + } else if (method == "DELETE") { + return (Method::HTTP_DELETE); + } else if (method == "OPTIONS") { + return (Method::HTTP_OPTIONS); + } else if (method == "CONNECT") { + return (Method::HTTP_CONNECT); + } else { + isc_throw(HttpRequestError, "unknown HTTP method " << method); + } +} + +std::string +HttpRequest::methodToString(const HttpRequest::Method& method) const { + switch (method) { + case Method::HTTP_GET: + return ("GET"); + case Method::HTTP_POST: + return ("POST"); + case Method::HTTP_HEAD: + return ("HEAD"); + case Method::HTTP_PUT: + return ("PUT"); + case Method::HTTP_DELETE: + return ("DELETE"); + case Method::HTTP_OPTIONS: + return ("OPTIONS"); + case Method::HTTP_CONNECT: + return ("CONNECT"); + default: + return ("unknown HTTP method"); + } +} + +} +} diff --git a/src/lib/http/request.h b/src/lib/http/request.h new file mode 100644 index 0000000000..802d02bc32 --- /dev/null +++ b/src/lib/http/request.h @@ -0,0 +1,277 @@ +// Copyright (C) 2016 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/. + +#ifndef HTTP_REQUEST_H +#define HTTP_REQUEST_H + +#include <exceptions/exceptions.h> +#include <http/request_context.h> +#include <map> +#include <set> +#include <stdint.h> +#include <string> +#include <utility> + +namespace isc { +namespace http { + +/// @brief Generic exception thrown by @ref HttpRequest class. +class HttpRequestError : public Exception { +public: + HttpRequestError(const char* file, size_t line, const char* what) : + isc::Exception(file, line, what) { }; +}; + +/// @brief Exception thrown when attempt is made to retrieve a +/// non-existing header. +class HttpRequestNonExistingHeader : public HttpRequestError { +public: + HttpRequestNonExistingHeader(const char* file, size_t line, + const char* what) : + HttpRequestError(file, line, what) { }; +}; + +/// @brief Represents HTTP request message. +/// +/// This object represents parsed HTTP message. The @ref HttpRequestContext +/// contains raw data used as input for this object. This class interprets the +/// data. In particular, it verifies that the appropriate method, HTTP version, +/// and headers were used. The derivations of this class provide specializations +/// and specify the HTTP methods, versions and headers supported/required in +/// the specific use cases. +/// +/// For example, the @ref PostHttpRequest class derives from @ref HttpRequest +/// and it requires that parsed messages use POST method. The +/// @ref PostHttpRequestJson, which derives from @ref PostHttpRequest requires +/// that the POST message includes body holding a JSON structure and provides +/// methods to parse the JSON body. +class HttpRequest { +public: + + /// @brief Type of HTTP version, including major and minor version number. + typedef std::pair<unsigned int, unsigned int> HttpVersion; + + /// @brief HTTP methods. + enum class Method { + HTTP_GET, + HTTP_POST, + HTTP_HEAD, + HTTP_PUT, + HTTP_DELETE, + HTTP_OPTIONS, + HTTP_CONNECT, + HTTP_METHOD_UNKNOWN + }; + + /// @brief Constructor. + /// + /// Creates new context (@ref HttpRequestContext). + HttpRequest(); + + /// @brief Destructor. + virtual ~HttpRequest(); + + /// @brief Returns reference to the @ref HttpRequestContext. + /// + /// This method is called by the @ref HttpRequestParser to retrieve the + /// context in which parsed data is stored. + const HttpRequestContextPtr& context() const { + return (context_); + } + + /// @brief Specifies an HTTP method allowed for the request. + /// + /// Allowed methods must be specified prior to calling @ref create method. + /// If no method is specified, all methods are supported. + /// + /// @param method HTTP method allowed for the request. + void requireHttpMethod(const HttpRequest::Method& method); + + /// @brief Specifies HTTP version allowed. + /// + /// Allowed HTTP versions must be specified prior to calling @ref create + /// method. If no version is specified, all versions are allowed. + /// + /// @param version Version number allowed for the request. + void requireHttpVersion(const HttpVersion& version); + + /// @brief Specifies a required HTTP header for the request. + /// + /// Required headers must be specified prior to calling @ref create method. + /// The specified header must exist in the received HTTP request. This puts + /// no requirement on the header value. + /// + /// @param header_name Required header name. + void requireHeader(const std::string& header_name); + + /// @brief Specifies a required value of a header in the request. + /// + /// Required header values must be specified prior to calling @ref create + /// method. The specified header must exist and its value must be equal to + /// the value specified as second parameter. + /// + /// @param header_name HTTP header name. + /// @param header_value HTTP header valuae. + void requireHeaderValue(const std::string& header_name, + const std::string& header_value); + + /// @brief Checks if the body is required for the HTTP request. + /// + /// Current implementation simply checks if the "Content-Length" header + /// is required for the request. + /// + /// @return true if the body is required for this request. + bool requiresBody() const; + + /// @brief Reads parsed request from the @ref HttpRequestContext, validates + /// the request and stores parsed information. + /// + /// This method must be called before retrieving parsed data using accessors + /// such as @ref getMethod, @ref getUri etc. + /// + /// This method doesn't parse the HTTP request body. + /// + /// @throw HttpRequestError if the parsed request doesn't meet the specified + /// requirements for it. + virtual void create(); + + /// @brief Complete parsing of the HTTP request. + /// + /// HTTP request parsing is performed in two stages: HTTP headers, then + /// request body. The @ref create method parses HTTP headers. Once this is + /// done, the caller can check if the "Content-Length" was specified and use + /// it's value to determine the size of the body which is parsed in the + /// second stage. + /// + /// This method generally performs the body parsing, but if it determines + /// that the @ref create method hasn't been called, it calls @ref create + /// before parsing the body. + /// + /// The derivations must call @ref create if it hasn't been called prior to + /// calling this method. It must set @ref finalized_ to true if the call + /// to @ref finalize was successful. + virtual void finalize(); + + /// @brief Reset the state of the object. + virtual void reset(); + + /// @name HTTP data accessors. + /// + //@{ + /// @brief Returns HTTP method of the request. + Method getMethod() const; + + /// @brief Returns HTTP request URI. + std::string getUri() const; + + /// @brief Returns HTTP version number (major and minor). + HttpVersion getHttpVersion() const; + + /// @brief Returns a value of the specified HTTP header. + /// + /// @param header Name of the HTTP header. + /// + /// @throw HttpRequestError if the header doesn't exist. + std::string getHeaderValue(const std::string& header) const; + + /// @brief Returns a value of the specified HTTP header as number. + /// + /// @param header Name of the HTTP header. + /// + /// @throw HttpRequestError if the header doesn't exist or if the + /// header value is not number. + uint64_t getHeaderValueAsUint64(const std::string& header) const; + + /// @brief Returns HTTP message body as string. + std::string getBody() const; + + //@} + +protected: + + /// @brief Checks if the @ref create was called. + /// + /// @throw HttpRequestError if @ref create wasn't called. + void checkCreated() const; + + /// @brief Checks if the @ref finalize was called. + /// + /// @throw HttpRequestError if @ref finalize wasn't called. + void checkFinalized() const; + + /// @brief Checks if the set is empty or the specified element belongs + /// to this set. + /// + /// This is a convenience method used by the class to verify that the + /// given HTTP method belongs to "required methods", HTTP version belongs + /// to "required versions" etc. + /// + /// @param element Reference to the element. + /// @param element_set Reference to the set of elements. + /// @tparam Element type, e.g. @ref Method, @ref HttpVersion etc. + /// + /// @return true if the element set is empty or if the element belongs + /// to the set. + template<typename T> + bool inRequiredSet(const T& element, + const std::set<T>& element_set) const; + + /// @brief Converts HTTP method specified in textual format to @ref Method. + /// + /// @param method HTTP method specified in the textual format. This value + /// is case insensitive. + /// + /// @return HTTP method as enum. + /// @throw HttpRequestError if unknown method specified. + Method methodFromString(std::string method) const; + + /// @brief Converts HTTP method to string. + /// + /// @param method HTTP method specified as enum. + /// + /// @return HTTP method as string. + std::string methodToString(const HttpRequest::Method& method) const; + + /// @brief Set of required HTTP methods. + /// + /// If the set is empty, all methods are allowed. + std::set<Method> required_methods_; + + /// @brief Set of required HTTP versions. + /// + /// If the set is empty, all versions are allowed. + std::set<HttpVersion> required_versions_; + + /// @brief Map holding required HTTP headers. + /// + /// The key of this map specifies the HTTP header name. The value + /// spefifies the HTTP header value. If the value is empty, the + /// header is required but the value of the header is not checked. + /// If the value is non-empty, the value in the HTTP request must + /// be equal to the value in the map. + std::map<std::string, std::string> required_headers_; + + /// @brief Flag indicating whether @ref create was called. + bool created_; + + /// @brief Flag indicating whether @ref finalize was called. + bool finalized_; + + /// @brief HTTP method of the request. + Method method_; + + /// @brief Parsed HTTP headers. + std::map<std::string, std::string> headers_; + + /// @brief Pointer to the @ref HttpRequestContext holding parsed + /// data. + HttpRequestContextPtr context_; +}; + +} +} + +#endif diff --git a/src/lib/http/request_context.h b/src/lib/http/request_context.h new file mode 100644 index 0000000000..bcbacdac43 --- /dev/null +++ b/src/lib/http/request_context.h @@ -0,0 +1,44 @@ +// Copyright (C) 2016 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/. + +#ifndef HTTP_REQUEST_CONTEXT_H +#define HTTP_REQUEST_CONTEXT_H + +#include <http/header_context.h> +#include <boost/shared_ptr.hpp> +#include <string> +#include <vector> + +namespace isc { +namespace http { + +/// @brief HTTP request context. +/// +/// The context is used by the @ref HttpRequestParser to store parsed +/// data. This data is later used to create an instance of the +/// @ref HttpRequest or its derivation. +struct HttpRequestContext { + /// @brief HTTP request method. + std::string method_; + /// @brief HTTP request URI. + std::string uri_; + /// @brief HTTP major version number. + unsigned int http_version_major_; + /// @brief HTTP minor version number. + unsigned int http_version_minor_; + /// @brief Collection of HTTP headers. + std::vector<HttpHeaderContext> headers_; + /// @brief HTTP request body. + std::string body_; +}; + +/// @brief Pointer to the @ref HttpRequestContext. +typedef boost::shared_ptr<HttpRequestContext> HttpRequestContextPtr; + +} // namespace http +} // namespace isc + +#endif diff --git a/src/lib/http/request_parser.cc b/src/lib/http/request_parser.cc new file mode 100644 index 0000000000..c76434991b --- /dev/null +++ b/src/lib/http/request_parser.cc @@ -0,0 +1,677 @@ +// Copyright (C) 2016 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 <http/request_parser.h> +#include <boost/bind.hpp> +#include <cctype> +#include <iostream> + +using namespace isc::util; + +namespace isc { +namespace http { + +const int HttpRequestParser::RECEIVE_START_ST; +const int HttpRequestParser::HTTP_METHOD_ST; +const int HttpRequestParser::HTTP_URI_ST; +const int HttpRequestParser::HTTP_VERSION_H_ST; +const int HttpRequestParser::HTTP_VERSION_T1_ST; +const int HttpRequestParser::HTTP_VERSION_T2_ST; +const int HttpRequestParser::HTTP_VERSION_P_ST; +const int HttpRequestParser::HTTP_VERSION_SLASH_ST; +const int HttpRequestParser::HTTP_VERSION_MAJOR_START_ST; +const int HttpRequestParser::HTTP_VERSION_MAJOR_ST; +const int HttpRequestParser::HTTP_VERSION_MINOR_START_ST; +const int HttpRequestParser::HTTP_VERSION_MINOR_ST; +const int HttpRequestParser::EXPECTING_NEW_LINE1_ST; +const int HttpRequestParser::HEADER_LINE_START_ST; +const int HttpRequestParser::HEADER_LWS_ST; +const int HttpRequestParser::HEADER_NAME_ST; +const int HttpRequestParser::SPACE_BEFORE_HEADER_VALUE_ST; +const int HttpRequestParser::HEADER_VALUE_ST; +const int HttpRequestParser::EXPECTING_NEW_LINE2_ST; +const int HttpRequestParser::EXPECTING_NEW_LINE3_ST; +const int HttpRequestParser::HTTP_BODY_ST; +const int HttpRequestParser::HTTP_PARSE_OK_ST; +const int HttpRequestParser::HTTP_PARSE_FAILED_ST; + +const int HttpRequestParser::DATA_READ_OK_EVT; +const int HttpRequestParser::NEED_MORE_DATA_EVT; +const int HttpRequestParser::MORE_DATA_PROVIDED_EVT; +const int HttpRequestParser::HTTP_PARSE_OK_EVT; +const int HttpRequestParser::HTTP_PARSE_FAILED_EVT; + +HttpRequestParser::HttpRequestParser(HttpRequest& request) + : StateModel(), buffer_(), request_(request), + context_(request_.context()), error_message_() { +} + +void +HttpRequestParser::initModel() { + // Intialize dictionaries of events and states. + initDictionaries(); + + // Set the current state to starting state and enter the run loop. + setState(RECEIVE_START_ST); + + // Parsing starts from here. + postNextEvent(START_EVT); +} + +void +HttpRequestParser::poll() { + try { + // Run the parser until it runs out of input data or until + // parsing completes. + do { + getState(getCurrState())->run(); + + } while (!isModelDone() && (getNextEvent() != NOP_EVT) && + (getNextEvent() != NEED_MORE_DATA_EVT)); + } catch (const std::exception& ex) { + abortModel(ex.what()); + } +} + +bool +HttpRequestParser::needData() const { + return (getNextEvent() == NEED_MORE_DATA_EVT); +} + +bool +HttpRequestParser::httpParseOk() const { + return ((getNextEvent() == END_EVT) && + (getLastEvent() == HTTP_PARSE_OK_EVT)); +} + +void +HttpRequestParser::postBuffer(const void* buf, const size_t buf_size) { + if (buf_size > 0) { + // The next event is NEED_MORE_DATA_EVT when the parser wants to + // signal that more data is needed. This method is called to supply + // more data and thus it should change the next event to + // MORE_DATA_PROVIDED_EVT. + if (getNextEvent() == NEED_MORE_DATA_EVT) { + transition(getCurrState(), MORE_DATA_PROVIDED_EVT); + } + buffer_.insert(buffer_.end(), static_cast<const uint8_t*>(buf), + static_cast<const uint8_t*>(buf) + buf_size); + } +} + +void +HttpRequestParser::defineEvents() { + StateModel::defineEvents(); + + // Define HTTP parser specific events. + defineEvent(DATA_READ_OK_EVT, "DATA_READ_OK_EVT"); + defineEvent(NEED_MORE_DATA_EVT, "NEED_MORE_DATA_EVT"); + defineEvent(MORE_DATA_PROVIDED_EVT, "MORE_DATA_PROVIDED_EVT"); + defineEvent(HTTP_PARSE_OK_EVT, "HTTP_PARSE_OK_EVT"); + defineEvent(HTTP_PARSE_FAILED_EVT, "HTTP_PARSE_FAILED_EVT"); +} + +void +HttpRequestParser::verifyEvents() { + StateModel::verifyEvents(); + + getEvent(DATA_READ_OK_EVT); + getEvent(NEED_MORE_DATA_EVT); + getEvent(MORE_DATA_PROVIDED_EVT); + getEvent(HTTP_PARSE_OK_EVT); + getEvent(HTTP_PARSE_FAILED_EVT); +} + +void +HttpRequestParser::defineStates() { + // Call parent class implementation first. + StateModel::defineStates(); + + // Define HTTP parser specific states. + defineState(RECEIVE_START_ST, "RECEIVE_START_ST", + boost::bind(&HttpRequestParser::receiveStartHandler, this)); + + defineState(HTTP_METHOD_ST, "HTTP_METHOD_ST", + boost::bind(&HttpRequestParser::httpMethodHandler, this)); + + defineState(HTTP_URI_ST, "HTTP_URI_ST", + boost::bind(&HttpRequestParser::uriHandler, this)); + + defineState(HTTP_VERSION_H_ST, "HTTP_VERSION_H_ST", + boost::bind(&HttpRequestParser::versionHTTPHandler, this, 'H', + HTTP_VERSION_T1_ST)); + + defineState(HTTP_VERSION_T1_ST, "HTTP_VERSION_T1_ST", + boost::bind(&HttpRequestParser::versionHTTPHandler, this, 'T', + HTTP_VERSION_T2_ST)); + + defineState(HTTP_VERSION_T2_ST, "HTTP_VERSION_T2_ST", + boost::bind(&HttpRequestParser::versionHTTPHandler, this, 'T', + HTTP_VERSION_P_ST)); + + defineState(HTTP_VERSION_P_ST, "HTTP_VERSION_P_ST", + boost::bind(&HttpRequestParser::versionHTTPHandler, this, 'P', + HTTP_VERSION_SLASH_ST)); + + defineState(HTTP_VERSION_SLASH_ST, "HTTP_VERSION_SLASH_ST", + boost::bind(&HttpRequestParser::versionHTTPHandler, this, '/', + HTTP_VERSION_MAJOR_ST)); + + defineState(HTTP_VERSION_MAJOR_START_ST, "HTTP_VERSION_MAJOR_START_ST", + boost::bind(&HttpRequestParser::versionNumberStartHandler, this, + HTTP_VERSION_MAJOR_ST, + &context_->http_version_major_)); + + defineState(HTTP_VERSION_MAJOR_ST, "HTTP_VERSION_MAJOR_ST", + boost::bind(&HttpRequestParser::versionNumberHandler, this, + '.', HTTP_VERSION_MINOR_START_ST, + &context_->http_version_major_)); + + defineState(HTTP_VERSION_MINOR_START_ST, "HTTP_VERSION_MINOR_START_ST", + boost::bind(&HttpRequestParser::versionNumberStartHandler, this, + HTTP_VERSION_MINOR_ST, + &context_->http_version_minor_)); + + defineState(HTTP_VERSION_MINOR_ST, "HTTP_VERSION_MINOR_ST", + boost::bind(&HttpRequestParser::versionNumberHandler, this, + '\r', EXPECTING_NEW_LINE1_ST, + &context_->http_version_minor_)); + + defineState(EXPECTING_NEW_LINE1_ST, "EXPECTING_NEW_LINE1_ST", + boost::bind(&HttpRequestParser::expectingNewLineHandler, this, + HEADER_LINE_START_ST)); + + defineState(HEADER_LINE_START_ST, "HEADER_LINE_START_ST", + boost::bind(&HttpRequestParser::headerLineStartHandler, this)); + + defineState(HEADER_LWS_ST, "HEADER_LWS_ST", + boost::bind(&HttpRequestParser::headerLwsHandler, this)); + + defineState(HEADER_NAME_ST, "HEADER_NAME_ST", + boost::bind(&HttpRequestParser::headerNameHandler, this)); + + defineState(SPACE_BEFORE_HEADER_VALUE_ST, "SPACE_BEFORE_HEADER_VALUE_ST", + boost::bind(&HttpRequestParser::spaceBeforeHeaderValueHandler, this)); + + defineState(HEADER_VALUE_ST, "HEADER_VALUE_ST", + boost::bind(&HttpRequestParser::headerValueHandler, this)); + + defineState(EXPECTING_NEW_LINE2_ST, "EXPECTING_NEW_LINE2", + boost::bind(&HttpRequestParser::expectingNewLineHandler, this, + HEADER_LINE_START_ST)); + + defineState(EXPECTING_NEW_LINE3_ST, "EXPECTING_NEW_LINE3_ST", + boost::bind(&HttpRequestParser::expectingNewLineHandler, this, + HTTP_PARSE_OK_ST)); + + defineState(HTTP_BODY_ST, "HTTP_BODY_ST", + boost::bind(&HttpRequestParser::bodyHandler, this)); + + defineState(HTTP_PARSE_OK_ST, "HTTP_PARSE_OK_ST", + boost::bind(&HttpRequestParser::parseEndedHandler, this)); + + defineState(HTTP_PARSE_FAILED_ST, "HTTP_PARSE_FAILED_ST", + boost::bind(&HttpRequestParser::parseEndedHandler, this)); +} + +void +HttpRequestParser::parseFailure(const std::string& error_msg) { + error_message_ = error_msg + " : " + getContextStr(); + transition(HTTP_PARSE_FAILED_ST, HTTP_PARSE_FAILED_EVT); +} + +void +HttpRequestParser::onModelFailure(const std::string& explanation) { + if (error_message_.empty()) { + error_message_ = explanation; + } +} + +char +HttpRequestParser::getNextFromBuffer() { + unsigned int ev = getNextEvent(); + char c = '\0'; + // The caller should always provide additional data when the + // NEED_MORE_DATA_EVT occurrs. If the next event is still + // NEED_MORE_DATA_EVT it indicates that the caller hasn't provided + // the data. + if (ev == NEED_MORE_DATA_EVT) { + isc_throw(HttpRequestParserError, + "HTTP request parser requires new data to progress, but no data" + " have been provided. The transaction is aborted to avoid" + " a deadlock. This is a Kea HTTP server logic error!"); + + } else { + // Try to pop next character from the buffer. + const bool data_exist = popNextFromBuffer(c); + if (!data_exist) { + // There is no more data so it is really not possible that we're + // at MORE_DATA_PROVIDED_EVT. + if (ev == MORE_DATA_PROVIDED_EVT) { + isc_throw(HttpRequestParserError, + "HTTP server state indicates that new data have been" + " provided to be parsed, but the transaction buffer" + " contains no new data. This is a Kea HTTP server logic" + " error!"); + + } else { + // If there is no more data we should set NEED_MORE_DATA_EVT + // event to indicate that new data should be provided. + transition(getCurrState(), NEED_MORE_DATA_EVT); + } + } + } + return (c); +} + +void +HttpRequestParser::invalidEventError(const std::string& handler_name, + const unsigned int event) { + isc_throw(HttpRequestParserError, handler_name << ": " + << " invalid event " << getEventLabel(static_cast<int>(event))); +} + +void +HttpRequestParser::stateWithReadHandler(const std::string& handler_name, + boost::function<void(const char c)> + after_read_logic) { + char c = getNextFromBuffer(); + // Do nothing if we reached the end of buffer. + if (getNextEvent() != NEED_MORE_DATA_EVT) { + switch(getNextEvent()) { + case DATA_READ_OK_EVT: + case MORE_DATA_PROVIDED_EVT: + after_read_logic(c); + break; + default: + invalidEventError(handler_name, getNextEvent()); + } + } +} + + +void +HttpRequestParser::receiveStartHandler() { + char c = getNextFromBuffer(); + if (getNextEvent() != NEED_MORE_DATA_EVT) { + switch(getNextEvent()) { + case START_EVT: + // The first byte should contain a first character of the + // HTTP method name. + if (!isChar(c) || isCtl(c) || isSpecial(c)) { + parseFailure("invalid first character " + std::string(1, c) + + " in HTTP method name"); + + } else { + context_->method_.push_back(c); + transition(HTTP_METHOD_ST, DATA_READ_OK_EVT); + } + break; + + default: + invalidEventError("receiveStartHandler", getNextEvent()); + } + } +} + +void +HttpRequestParser::httpMethodHandler() { + stateWithReadHandler("httpMethodHandler", [this](const char c) { + // Space character terminates the HTTP method name. Next thing + // is the URI. + if (c == ' ') { + transition(HTTP_URI_ST, DATA_READ_OK_EVT); + + } else if (!isChar(c) || isCtl(c) || isSpecial(c)) { + parseFailure("invalid character " + std::string(1, c) + + " in HTTP method name"); + + } else { + // Still parsing the method. Append the next character to the + // method name. + context_->method_.push_back(c); + transition(getCurrState(), DATA_READ_OK_EVT); + } + }); +} + +void +HttpRequestParser::uriHandler() { + stateWithReadHandler("uriHandler", [this](const char c) { + // Space character terminates the URI. + if (c == ' ') { + transition(HTTP_VERSION_H_ST, DATA_READ_OK_EVT); + + } else if (isCtl(c)) { + parseFailure("control character found in HTTP URI"); + transition(HTTP_PARSE_FAILED_ST, HTTP_PARSE_FAILED_EVT); + + } else { + // Still parsing the URI. Append the next character to the + // method name. + context_->uri_.push_back(c); + transition(HTTP_URI_ST, DATA_READ_OK_EVT); + } + }); +} + +void +HttpRequestParser::versionHTTPHandler(const char expected_letter, + const unsigned int next_state) { + stateWithReadHandler("versionHTTPHandler", + [this, expected_letter, next_state](const char c) { + // We're handling one of the letters: 'H', 'T' or 'P'. + if (c == expected_letter) { + // The HTTP version is specified as "HTTP/X.Y". If the current + // character is a slash we're starting to parse major HTTP version + // number. Let's reset the version numbers. + if (c == '/') { + context_->http_version_major_ = 0; + context_->http_version_minor_ = 0; + } + // In all cases, let's transition to next specified state. + transition(next_state, DATA_READ_OK_EVT); + + } else { + // Unexpected character found. Parsing fails. + parseFailure("unexpected character " + std::string(1, c) + + " in HTTP version string"); + } + }); +} + +void +HttpRequestParser::versionNumberStartHandler(const unsigned int next_state, + unsigned int* storage) { + stateWithReadHandler("versionNumberStartHandler", + [this, next_state, storage](const char c) mutable { + // HTTP version number must be a digit. + if (isdigit(c)) { + // Update the version number using new digit being parsed. + *storage = *storage * 10 + c - '0'; + transition(next_state, DATA_READ_OK_EVT); + + } else { + parseFailure("expected digit in HTTP version, found " + + std::string(1, c)); + transition(HTTP_PARSE_FAILED_ST, HTTP_PARSE_FAILED_EVT); + } + }); +} + +void +HttpRequestParser::versionNumberHandler(const char following_character, + const unsigned int next_state, + unsigned int* const storage) { + stateWithReadHandler("versionNumberHandler", + [this, following_character, next_state, storage](const char c) + mutable { + // We're getting to the end of the version number, let's transition + // to next state. + if (c == following_character) { + transition(next_state, DATA_READ_OK_EVT); + + } else if (isdigit(c)) { + // Current character is a digit, so update the version number. + *storage = *storage * 10 + c - '0'; + + } else { + parseFailure("expected digit in HTTP version, found " + + std::string(1, c)); + transition(HTTP_PARSE_FAILED_ST, HTTP_PARSE_FAILED_EVT); + } + }); +} + +void +HttpRequestParser::expectingNewLineHandler(const unsigned int next_state) { + stateWithReadHandler("expectingNewLineHandler", [this, next_state](const char c) { + // Only a new line character is allowed in this state. + if (c == '\n') { + // If next state is HTTP_PARSE_OK_ST it means that we're + // parsing 3rd new line in the HTTP request message. This + // terminates the HTTP request (if there is no body) or marks the + // beginning of the body. + if (next_state == HTTP_PARSE_OK_ST) { + // Whether there is a body in this message or not, we should + // parse the HTTP headers to validate it and to check if there + // is "Content-Length" specified. The "Content-Length" is + // required for parsing body. + request_.create(); + try { + // This will throw exception if there is no Content-Length. + uint64_t content_length = + request_.getHeaderValueAsUint64("Content-Length"); + if (content_length > 0) { + // There is body in this request, so let's parse it. + transition(HTTP_BODY_ST, DATA_READ_OK_EVT); + } + } catch (const std::exception& ex) { + // There is no body in this message. If the body is required + // parsing fails. + if (request_.requiresBody()) { + parseFailure("HTTP message lacks a body"); + + } else { + // Body not required so simply terminate parsing. + transition(HTTP_PARSE_OK_ST, HTTP_PARSE_OK_EVT); + } + } + + } else { + // This is 1st or 2nd new line, so let's transition to the + // next state required by this handler. + transition(next_state, DATA_READ_OK_EVT); + } + } else { + parseFailure("expecting new line after CR, found " + + std::string(1, c)); + } + }); +} + +void +HttpRequestParser::headerLineStartHandler() { + stateWithReadHandler("headerLineStartHandler", [this](const char c) { + // If we're parsing HTTP headers and we found CR it marks the + // end of headers section. + if (c == '\r') { + transition(EXPECTING_NEW_LINE3_ST, DATA_READ_OK_EVT); + + } else if (!context_->headers_.empty() && ((c == ' ') || (c == '\t'))) { + // New line in headers section followed by space or tab is an LWS, + // a line break within header value. + transition(HEADER_LWS_ST, DATA_READ_OK_EVT); + + } else if (!isChar(c) || isCtl(c) || isSpecial(c)) { + parseFailure("invalid character " + std::string(1, c) + + " in header name"); + + } else { + // Update header name with the parse letter. + context_->headers_.push_back(HttpHeaderContext()); + context_->headers_.back().name_.push_back(c); + transition(HEADER_NAME_ST, DATA_READ_OK_EVT); + } + }); +} + +void +HttpRequestParser::headerLwsHandler() { + stateWithReadHandler("headerLwsHandler", [this](const char c) { + if (c == '\r') { + // Found CR during parsing a header value. Next value + // should be new line. + transition(EXPECTING_NEW_LINE2_ST, DATA_READ_OK_EVT); + + } else if ((c == ' ') || (c == '\t')) { + // Space and tab is used to mark LWS. Simply swallow + // this character. + transition(getCurrState(), DATA_READ_OK_EVT); + + } else if (isCtl(c)) { + parseFailure("control character found in the HTTP header " + + context_->headers_.back().name_); + + } else { + // We're parsing header value, so let's update it. + context_->headers_.back().value_.push_back(c); + transition(HEADER_VALUE_ST, DATA_READ_OK_EVT); + } + }); +} + +void +HttpRequestParser::headerNameHandler() { + stateWithReadHandler("headerNameHandler", [this](const char c) { + // Colon follows header name and it has its own state. + if (c == ':') { + transition(SPACE_BEFORE_HEADER_VALUE_ST, DATA_READ_OK_EVT); + + } else if (!isChar(c) || isCtl(c) || isSpecial(c)) { + parseFailure("invalid character " + std::string(1, c) + + " found in the HTTP header name"); + + } else { + // Parsing a header name, so update it. + context_->headers_.back().name_.push_back(c); + transition(getCurrState(), DATA_READ_OK_EVT); + } + }); +} + +void +HttpRequestParser::spaceBeforeHeaderValueHandler() { + stateWithReadHandler("spaceBeforeHeaderValueHandler", [this](const char c) { + if (c == ' ') { + // Remove leading whitespace from the header value. + transition(getCurrState(), DATA_READ_OK_EVT); + + } else if (c == '\r') { + // If CR found during parsing header value, it marks the end + // of this value. + transition(EXPECTING_NEW_LINE2_ST, DATA_READ_OK_EVT); + + } else if (isCtl(c)) { + parseFailure("control character found in the HTTP header " + + context_->headers_.back().name_); + + } else { + // Still parsing the value, so let's update it. + context_->headers_.back().value_.push_back(c); + transition(HEADER_VALUE_ST, DATA_READ_OK_EVT); + } + }); +} + +void +HttpRequestParser::headerValueHandler() { + stateWithReadHandler("headerValueHandler", [this](const char c) { + // If CR found during parsing header value, it marks the end + // of this value. + if (c == '\r') { + transition(EXPECTING_NEW_LINE2_ST, DATA_READ_OK_EVT); + + } else if (isCtl(c)) { + parseFailure("control character found in the HTTP header " + + context_->headers_.back().name_); + + } else { + // Still parsing the value, so let's update it. + context_->headers_.back().value_.push_back(c); + transition(HEADER_VALUE_ST, DATA_READ_OK_EVT); + } + }); +} + +void +HttpRequestParser::bodyHandler() { + stateWithReadHandler("bodyHandler", [this](const char c) { + // We don't validate the body at this stage. Simply record the + // number of characters specified within "Content-Length". + context_->body_.push_back(c); + if (context_->body_.length() < + request_.getHeaderValueAsUint64("Content-Length")) { + transition(HTTP_BODY_ST, DATA_READ_OK_EVT); + } else { + transition(HTTP_PARSE_OK_ST, HTTP_PARSE_OK_EVT); + } + }); +} + + +void +HttpRequestParser::parseEndedHandler() { + switch(getNextEvent()) { + case HTTP_PARSE_OK_EVT: + request_.finalize(); + transition(END_ST, END_EVT); + break; + case HTTP_PARSE_FAILED_EVT: + abortModel("HTTP request parsing failed"); + break; + + default: + invalidEventError("parseEndedHandler", getNextEvent()); + } +} + +bool +HttpRequestParser::popNextFromBuffer(char& next) { + // If there are any characters in the buffer, pop next. + if (!buffer_.empty()) { + next = buffer_.front(); + buffer_.pop_front(); + return (true); + } + return (false); +} + + +bool +HttpRequestParser::isChar(const char c) const { + return ((c >= 0) && (c <= 127)); +} + +bool +HttpRequestParser::isCtl(const char c) const { + return (((c >= 0) && (c <= 31)) || (c == 127)); +} + +bool +HttpRequestParser::isSpecial(const char c) const { + switch (c) { + case '(': + case ')': + case '<': + case '>': + case '@': + case ',': + case ';': + case ':': + case '\\': + case '"': + case '/': + case '[': + case ']': + case '?': + case '=': + case '{': + case '}': + case ' ': + case '\t': + return true; + + default: + ; + } + + return false; +} + + +} // namespace http +} // namespace isc diff --git a/src/lib/http/request_parser.h b/src/lib/http/request_parser.h new file mode 100644 index 0000000000..a1b9fcd5e9 --- /dev/null +++ b/src/lib/http/request_parser.h @@ -0,0 +1,450 @@ +// Copyright (C) 2016 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/. + +#ifndef HTTP_REQUEST_PARSER_H +#define HTTP_REQUEST_PARSER_H + +#include <exceptions/exceptions.h> +#include <http/request.h> +#include <util/state_model.h> +#include <boost/function.hpp> +#include <list> +#include <stdint.h> +#include <string> + +namespace isc { +namespace http { + +/// @brief Exception thrown when an error during parsing HTTP request +/// has occurred. +/// +/// The most common errors are due to receiving malformed requests. +class HttpRequestParserError : public Exception { +public: + HttpRequestParserError(const char* file, size_t line, const char* what) : + isc::Exception(file, line, what) { }; +}; + +/// @brief A generic parser for HTTP requests. +/// +/// This class implements a parser for HTTP requests. The parser derives from +/// @ref isc::util::StateModel class and implements its own state machine on +/// top of it. The states of the parser reflect various parts of the HTTP +/// message being parsed, e.g. parsing HTTP method, parsing URI, parsing +/// message body etc. The descriptions of all parser states are provided +/// below together with the constants defining these states. +/// +/// HTTP uses TCP as a transport which is asynchronous in nature, i.e. the +/// HTTP message is received in chunks and multiple TCP connections can be +/// established at the same time. Multiplexing between these connections +/// requires providing a separate state machine per connection to "remeber" +/// the state of each transaction when the parser is waiting for asynchronous +/// data to be delivered. While the parser is waiting for the data, it can +/// parse requests received over other connections. This class provides means +/// for parsing partial data received over the specific connection and +/// interrupting data parsing to switch to a different context. +/// +/// The request parser validates the syntax of the received message as it +/// progresses with parsing the data. Though, it doesn't interpret the received +/// data until it parses the whole message. In most cases we want to apply some +/// restrictions on the message content, e.g. Kea Control API requires that +/// commands are sent using HTTP POST, with a JSON command being carried in a +/// message body. The parser doesn't verify if the message meets these +/// restrictions until the whole message is parsed, i.e. stored in the +/// @ref HttpRequestContext object. This object is associated with a +/// @ref HttpRequest object (or its derivation). When the parsing is completed, +/// the @ref HttpRequest::create method is called to retrieve the data from +/// the @ref HttpRequestContext and interpret the data. In particular, the +/// @ref HttpRequest or its derivation checks if the received message meets +/// desired restrictions. +/// +/// Kea Control API uses @ref PostHttpRequestJson class (which derives from the +/// @ref HttpRequest) to interpret received request. This class requires +/// that the HTTP request uses POST method and contains the following headers: +/// - Content-Type: application/json, +/// - Content-Length +/// +/// If any of these restrictions is not met in the received message, an +/// exception will be thrown, thereby @ref HttpRequestParser will fail parsing +/// the message. +/// +/// A new method @ref HttpRequestParser::poll has been created to run the +/// parser's state machine as long as there are unparsed data in the parser's +/// internal buffer. This method returns control to the caller when the parser +/// runs out of data in this buffer. The caller must feed the buffer by calling +/// @ref HttpRequestParser::postBuffer and then run @ref HttpRequestParser::poll +//// again. +/// +/// The @ref util::StateModel::runModel must not be used to run the +/// @ref HttpRequestParser state machine, thus it is made private method. +class HttpRequestParser : public util::StateModel { +public: + + /// @name States supported by the HttpRequestParser. + /// + //@{ + + /// @brief State indicating a beginning of parsing. + static const int RECEIVE_START_ST = SM_DERIVED_STATE_MIN + 1; + + /// @brief Parsing HTTP method, e.g. GET, POST etc. + static const int HTTP_METHOD_ST = SM_DERIVED_STATE_MIN + 2; + + /// @brief Parsing URI. + static const int HTTP_URI_ST = SM_DERIVED_STATE_MIN + 3; + + /// @brief Parsing letter "H" of "HTTP". + static const int HTTP_VERSION_H_ST = SM_DERIVED_STATE_MIN + 4; + + /// @brief Parsing first occurrence of "T" in "HTTP". + static const int HTTP_VERSION_T1_ST = SM_DERIVED_STATE_MIN + 5; + + /// @brief Parsing second occurrence of "T" in "HTTP". + static const int HTTP_VERSION_T2_ST = SM_DERIVED_STATE_MIN + 6; + + /// @brief Parsing letter "P" in "HTTP". + static const int HTTP_VERSION_P_ST = SM_DERIVED_STATE_MIN + 7; + + /// @brief Parsing slash character in "HTTP/Y.X" + static const int HTTP_VERSION_SLASH_ST = SM_DERIVED_STATE_MIN + 8; + + /// @brief Starting to parse major HTTP version number. + static const int HTTP_VERSION_MAJOR_START_ST = SM_DERIVED_STATE_MIN + 9; + + /// @brief Parsing major HTTP version number. + static const int HTTP_VERSION_MAJOR_ST = SM_DERIVED_STATE_MIN + 10; + + /// @brief Starting to parse minor HTTP version number. + static const int HTTP_VERSION_MINOR_START_ST = SM_DERIVED_STATE_MIN + 11; + + /// @brief Parsing minor HTTP version number. + static const int HTTP_VERSION_MINOR_ST = SM_DERIVED_STATE_MIN + 12; + + /// @brief Parsing first new line (after HTTP version number). + static const int EXPECTING_NEW_LINE1_ST = SM_DERIVED_STATE_MIN + 13; + + /// @brief Starting to parse a header line. + static const int HEADER_LINE_START_ST = SM_DERIVED_STATE_MIN + 14; + + /// @brief Parsing LWS (Linear White Space), i.e. new line with a space + /// or tab character while parsing a HTTP header. + static const int HEADER_LWS_ST = SM_DERIVED_STATE_MIN + 15; + + /// @brief Parsing header name. + static const int HEADER_NAME_ST = SM_DERIVED_STATE_MIN + 16; + + /// @brief Parsing space before header value. + static const int SPACE_BEFORE_HEADER_VALUE_ST = SM_DERIVED_STATE_MIN + 17; + + /// @brief Parsing header value. + static const int HEADER_VALUE_ST = SM_DERIVED_STATE_MIN + 18; + + /// @brief Expecting new line after parsing header value. + static const int EXPECTING_NEW_LINE2_ST = SM_DERIVED_STATE_MIN + 19; + + /// @brief Expecting second new line marking end of HTTP headers. + static const int EXPECTING_NEW_LINE3_ST = SM_DERIVED_STATE_MIN + 20; + + /// @brief Parsing body of a HTTP message. + static const int HTTP_BODY_ST = SM_DERIVED_STATE_MIN + 21; + + /// @brief Parsing successfully completed. + static const int HTTP_PARSE_OK_ST = SM_DERIVED_STATE_MIN + 100; + + /// @brief Parsing failed. + static const int HTTP_PARSE_FAILED_ST = SM_DERIVED_STATE_MIN + 101; + + //@} + + + /// @name Events used during HTTP message parsing. + /// + //@{ + + /// @brief Chunk of data successfully read and parsed. + static const int DATA_READ_OK_EVT = SM_DERIVED_EVENT_MIN + 1; + + /// @brief Unable to proceed with parsing until new data is provided. + static const int NEED_MORE_DATA_EVT = SM_DERIVED_EVENT_MIN + 2; + + /// @brief New data provided and parsing should continue. + static const int MORE_DATA_PROVIDED_EVT = SM_DERIVED_EVENT_MIN + 3; + + /// @brief Parsing HTTP request sucessfull. + static const int HTTP_PARSE_OK_EVT = SM_DERIVED_EVENT_MIN + 100; + + /// @brief Parsing HTTP request failed. + static const int HTTP_PARSE_FAILED_EVT = SM_DERIVED_EVENT_MIN + 101; + + //@} + + /// @brief Constructor. + /// + /// Creates new instance of the parser. + /// + /// @param request Reference to the @ref HttpRequest object or its + /// derivation that should be used to validate the parsed request and + /// to be used as a container for the parsed request. + HttpRequestParser(HttpRequest& request); + + /// @brief Initialize the state model for parsing. + /// + /// This method must be called before parsing the request, i.e. before + /// calling @ref HttpRequestParser::poll. It initializes dictionaries of + /// states and events and sets the initial model state to RECEIVE_START_ST. + void initModel(); + + /// @brief Run the parser as long as the amount of data is sufficient. + /// + /// The data to be parsed should be provided by calling + /// @ref HttpRequestParser::postBuffer. When the parser reaches the end of + /// the data buffer the @ref HttpRequestParser::poll sets the next event to + /// @ref NEED_MORE_DATA_EVT and returns. The caller should then invoke + /// @ref HttpRequestParser::postBuffer again to provide more data to the + /// parser, and call @ref HttpRequestParser::poll to continue parsing. + /// + /// This method also returns when parsing completes or fails. The last + /// event can be examined to check whether parsing was successful or not. + void poll(); + + /// @brief Returns true if the parser needs more data to continue. + /// + /// @return true if the next event is NEED_MORE_DATA_EVT. + bool needData() const; + + /// @brief Returns true if a request has been parsed successfully. + bool httpParseOk() const; + + /// @brief Returns error message. + std::string getErrorMessage() const { + return (error_message_); + } + + /// @brief Provides more input data to the parser. + /// + /// This method must be called prior to calling @ref HttpRequestParser::poll + /// to deliver data to be parsed. HTTP requests are received over TCP and + /// multiple reads may be necessary to retrieve the entire request. There is + /// no need to accumulate the entire request to start parsing it. A chunk + /// of data can be provided to the parser using this method and parsed right + /// away using @ref HttpRequestParser::poll. + /// + /// @param buf A pointer to the buffer holding the data. + /// @param buf_size Size of the data within the buffer. + void postBuffer(const void* buf, const size_t buf_size); + +private: + + /// @brief Make @ref runModel private to make sure that the caller uses + /// @ref poll method instead. + using StateModel::runModel; + + /// @brief Define events used by the parser. + virtual void defineEvents(); + + /// @brief Verifies events used by the parser. + virtual void verifyEvents(); + + /// @brief Defines states of the parser. + virtual void defineStates(); + + /// @brief Transition parser to failure state. + /// + /// This method transitions the parser to @ref HTTP_PARSE_FAILED_ST and + /// sets next event to HTTP_PARSE_FAILED_EVT. + /// + /// @param error_msg Error message explaining the failure. + void parseFailure(const std::string& error_msg); + + /// @brief A method called when parsing fails. + /// + /// @param explanation Error message explaining the reason for parsing + /// failure. + virtual void onModelFailure(const std::string& explanation); + + /// @brief Retrieves next byte of data from the buffer. + /// + /// During normal operation, when there is no more data in the buffer, + /// the parser sets NEED_MORE_DATA_EVT as next event to signal the need for + /// calling @ref HttpRequestParser::postBuffer. + /// + /// @throw HttpRequestParserError If current event is already set to + /// NEED_MORE_DATA_EVT or MORE_DATA_PROVIDED_EVT. In the former case, it + /// indicates that the caller failed to provide new data using + /// @ref HttpRequestParser::postBuffer. The latter case is highly unlikely + /// as it indicates that no new data were provided but the state of the + /// parser was changed from NEED_MORE_DATA_EVT or the data were provided + /// but the data buffer is empty. In both cases, it is an internal server + /// error. + char getNextFromBuffer(); + + /// @brief This method is called when invalid event occurred in a particular + /// parser state. + /// + /// This method simply throws @ref HttpRequestParserError informing about + /// invalid event occurring for the particular parser state. The error + /// message includes the name of the handler in which the exception + /// has been thrown. It also includes the event which caused the + /// exception. + /// + /// @param handler_name Name of the handler in which the exception is + /// thrown. + /// @param event An event which caused the exception. + /// + /// @throw HttpRequestParserError. + void invalidEventError(const std::string& handler_name, + const unsigned int event); + + /// @brief Generic parser handler which reads a single byte of data and + /// parses it using specified callback function. + /// + /// This generic handler is used in most of the parser states to parse a + /// single byte of input data. If there is no more data it simply returns. + /// Otherwise, if the next event is DATA_READ_OK_EVT or + /// MORE_DATA_PROVIDED_EVT, it calls the provided callback function to + /// parse the new byte of data. For all other states it throws an exception. + /// + /// @param handler_name Name of the handler function which called this + /// method. + /// @param after_read_logic Callback function to parse the byte of data. + /// This callback function implements state specific logic. + /// + /// @throw HttpRequestParserError when invalid event occurred. + void stateWithReadHandler(const std::string& handler_name, + boost::function<void(const char c)> + after_read_logic); + + /// @name State handlers. + /// + //@{ + + /// @brief Handler for RECEIVE_START_ST. + void receiveStartHandler(); + + /// @brief Handler for HTTP_METHOD_ST. + void httpMethodHandler(); + + /// @brief Handler for HTTP_URI_ST. + void uriHandler(); + + /// @brief Handler for states parsing "HTTP" string within the first line + /// of the HTTP request. + /// + /// @param expected_letter One of the 'H', 'T', 'P'. + /// @param next_state A state to which the parser should transition after + /// parsing the character. + void versionHTTPHandler(const char expected_letter, + const unsigned int next_state); + + /// @brief Handler for HTTP_VERSION_MAJOR_START_ST and + /// HTTP_VERSION_MINOR_START_ST. + /// + /// This handler calculates version number using the following equation: + /// @code + /// storage = storage * 10 + c - '0'; + /// @endcode + /// + /// @param next_state State to which the parser should transition. + /// @param [out] storage Reference to a number holding current product of + /// parsing major or minor version number. + void versionNumberStartHandler(const unsigned int next_state, + unsigned int* storage); + + /// @brief Handler for HTTP_VERSION_MAJOR_ST and HTTP_VERSION_MINOR_ST. + /// + /// This handler calculates version number using the following equation: + /// @code + /// storage = storage * 10 + c - '0'; + /// @endcode + /// + /// @param following_character Character following the version number, i.e. + /// '.' for major version, \r for minor version. + /// @param next_state State to which the parser should transition. + /// @param [out] storage Pointer to a number holding current product of + /// parsing major or minor version number. + void versionNumberHandler(const char following_character, + const unsigned int next_state, + unsigned int* const storage); + + /// @brief Handler for states related to new lines. + /// + /// If the next_state is HTTP_PARSE_OK_ST it indicates that the parsed + /// value is a 3rd new line within request HTTP message. In this case the + /// handler calls @ref HttpRequest::create to validate the received message + /// (excluding body). The hander then reads the "Content-Length" header to + /// check if the request contains a body. If the "Content-Length" is greater + /// than zero, the parser transitions to HTTP_BODY_ST. If the + /// "Content-Length" doesn't exist the parser transitions to + /// HTTP_PARSE_OK_ST. + /// + /// @param next_state A state to which parser should transition. + void expectingNewLineHandler(const unsigned int next_state); + + /// @brief Handler for HEADER_LINE_START_ST. + void headerLineStartHandler(); + + /// @brief Handler for HEADER_LWS_ST. + void headerLwsHandler(); + + /// @brief Handler for HEADER_NAME_ST. + void headerNameHandler(); + + /// @brief Handler for SPACE_BEFORE_HEADER_VALUE_ST. + void spaceBeforeHeaderValueHandler(); + + /// @brief Handler for HEADER_VALUE_ST. + void headerValueHandler(); + + /// @brief Handler for HTTP_BODY_ST. + void bodyHandler(); + + /// @brief Handler for HTTP_PARSE_OK_ST and HTTP_PARSE_FAILED_ST. + /// + /// If parsing is successful, it calls @ref HttpRequest::create to validate + /// the HTTP request. In both cases it transitions the parser to the END_ST. + void parseEndedHandler(); + + /// @brief Tries to read next byte from buffer. + /// + /// @param [out] next A reference to the variable where read data should be + /// stored. + /// + /// @return true if character was sucessfully read, false otherwise. + bool popNextFromBuffer(char& next); + + /// @brief Checks if specified value is a character. + /// + /// @return true, if specified value is a character. + bool isChar(const char c) const; + + /// @brief Checks if specified value is a control value. + /// + /// @return true, if specified value is a control value. + bool isCtl(const char c) const; + + /// @brief Checks if specified value is a special character. + /// + /// @return true, if specified value is a special character. + bool isSpecial(const char c) const; + + /// @brief Internal buffer from which parser reads data. + std::list<char> buffer_; + + /// @brief Reference to the request object specified in the constructor. + HttpRequest& request_; + + /// @brief Pointer to the internal context of the @ref HttpRequest object. + HttpRequestContextPtr context_; + + /// @brief Error message set by @ref onModelFailure. + std::string error_message_; +}; + +} // namespace http +} // namespace isc + +#endif // HTTP_REQUEST_PARSER_H + diff --git a/src/lib/http/tests/Makefile.am b/src/lib/http/tests/Makefile.am index 4faa5f0af4..588cd125ea 100644 --- a/src/lib/http/tests/Makefile.am +++ b/src/lib/http/tests/Makefile.am @@ -20,7 +20,11 @@ TESTS = if HAVE_GTEST TESTS += libhttp_unittests -libhttp_unittests_SOURCES = run_unittests.cc +libhttp_unittests_SOURCES = post_request_json_unittests.cc +libhttp_unittests_SOURCES += request_parser_unittests.cc +libhttp_unittests_SOURCES += request_test.h +libhttp_unittests_SOURCES += request_unittests.cc +libhttp_unittests_SOURCES += run_unittests.cc libhttp_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) libhttp_unittests_CXXFLAGS = $(AM_CXXFLAGS) @@ -29,6 +33,7 @@ libhttp_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS) libhttp_unittests_LDADD = $(top_builddir)/src/lib/http/libkea-http.la libhttp_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la libhttp_unittests_LDADD += $(top_builddir)/src/lib/log/libkea-log.la +libhttp_unittests_LDADD += $(top_builddir)/src/lib/cc/libkea-cc.la libhttp_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la libhttp_unittests_LDADD += $(LOG4CPLUS_LIBS) libhttp_unittests_LDADD += $(BOOST_LIBS) $(GTEST_LDADD) diff --git a/src/lib/http/tests/post_request_json_unittests.cc b/src/lib/http/tests/post_request_json_unittests.cc new file mode 100644 index 0000000000..89668d536d --- /dev/null +++ b/src/lib/http/tests/post_request_json_unittests.cc @@ -0,0 +1,172 @@ +// Copyright (C) 2016 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/post_request_json.h> +#include <http/tests/request_test.h> +#include <gtest/gtest.h> +#include <map> + +using namespace isc::data; +using namespace isc::http; +using namespace isc::http::test; + +namespace { + +/// @brief Test fixture class for @ref PostHttpRequestJson. +class PostHttpRequestJsonTest : + public HttpRequestTestBase<PostHttpRequestJson> { +public: + + /// @brief Constructor. + PostHttpRequestJsonTest() + : HttpRequestTestBase<PostHttpRequestJson>(), + json_body_("{ \"service\": \"dhcp4\", \"param1\": \"foo\" }") { + } + + /// @brief Sets new JSON body for the HTTP request context. + /// + /// If the body parameter is empty, it will use the value of + /// @ref json_body_ member. Otherwise, it will assign the body + /// provided as parameter. + /// + /// @param body new body value. + void setBody(const std::string& body = "") { + request_.context()->body_ = body.empty() ? json_body_ : body; + } + + /// @brief Default value of the JSON body. + std::string json_body_; + +}; + +// This test verifies that PostHttpRequestJson class only accepts +// POST messages. +TEST_F(PostHttpRequestJsonTest, requiredPost) { + // Use a GET method that is not supported. + setContextBasics("GET", "/isc/org", std::make_pair(1, 0)); + addHeaderToContext("Content-Length", json_body_.length()); + addHeaderToContext("Content-Type", "application/json"); + + ASSERT_THROW(request_.create(), HttpRequestError); + + // Now use POST. It should be accepted. + setContextBasics("POST", "/isc/org", std::make_pair(1, 0)); + addHeaderToContext("Content-Length", json_body_.length()); + addHeaderToContext("Content-Type", "application/json"); + + EXPECT_NO_THROW(request_.create()); +} + +// This test verifies that PostHttpRequest requires "Content-Length" +// header equal to "application/json". +TEST_F(PostHttpRequestJsonTest, requireContentTypeJson) { + // Specify "Content-Type" other than "application/json". + setContextBasics("POST", "/isc/org", std::make_pair(1, 0)); + addHeaderToContext("Content-Length", json_body_.length()); + addHeaderToContext("Content-Type", "text/html"); + + ASSERT_THROW(request_.create(), HttpRequestError); + + // This time specify correct "Content-Type". It should pass. + setContextBasics("POST", "/isc/org", std::make_pair(1, 0)); + addHeaderToContext("Content-Length", json_body_.length()); + addHeaderToContext("Content-Type", "application/json"); + + EXPECT_NO_THROW(request_.create()); +} + +// This test verifies that PostHttpRequest requires "Content-Length" +// header. +TEST_F(PostHttpRequestJsonTest, requireContentLength) { + // "Content-Length" is not specified initially. It should fail. + setContextBasics("POST", "/isc/org", std::make_pair(1, 0)); + addHeaderToContext("Content-Type", "text/html"); + + ASSERT_THROW(request_.create(), HttpRequestError); + + // Specify "Content-Length". It should pass. + setContextBasics("POST", "/isc/org", std::make_pair(1, 0)); + addHeaderToContext("Content-Length", json_body_.length()); + addHeaderToContext("Content-Type", "application/json"); +} + +// This test verifies that JSON body can be retrieved from the +// HTTP request. +TEST_F(PostHttpRequestJsonTest, getBodyAsJson) { + // Create HTTP POST request with JSON body. + setContextBasics("POST", "/isc/org", std::make_pair(1, 0)); + addHeaderToContext("Content-Length", json_body_.length()); + addHeaderToContext("Content-Type", "application/json"); + setBody(); + + ASSERT_NO_THROW(request_.finalize()); + + // Try to retrieve pointer to the root element of the JSON body. + ConstElementPtr json = request_.getBodyAsJson(); + ASSERT_TRUE(json); + + // Iterate over JSON values and store them in a simple map. + std::map<std::string, std::string> config_values; + for (auto config_element = json->mapValue().begin(); + config_element != json->mapValue().end(); + ++config_element) { + ASSERT_FALSE(config_element->first.empty()); + ASSERT_TRUE(config_element->second); + config_values[config_element->first] = config_element->second->stringValue(); + } + + // Verify the values. + EXPECT_EQ("dhcp4", config_values["service"]); + EXPECT_EQ("foo", config_values["param1"]); +} + +// This test verifies that an attempt to parse/retrieve malformed +// JSON structure will cause an exception. +TEST_F(PostHttpRequestJsonTest, getBodyAsJsonMalformed) { + setContextBasics("POST", "/isc/org", std::make_pair(1, 0)); + addHeaderToContext("Content-Length", json_body_.length()); + addHeaderToContext("Content-Type", "application/json"); + // No colon before 123. + setBody("{ \"command\" 123 }" ); + + EXPECT_THROW(request_.finalize(), HttpRequestJsonError); +} + +// This test verifies that NULL pointer is returned when trying to +// retrieve root element of the empty JSON structure. +TEST_F(PostHttpRequestJsonTest, getEmptyJsonBody) { + setContextBasics("POST", "/isc/org", std::make_pair(1, 0)); + addHeaderToContext("Content-Length", json_body_.length()); + addHeaderToContext("Content-Type", "application/json"); + + ASSERT_NO_THROW(request_.finalize()); + + ConstElementPtr json = request_.getBodyAsJson(); + EXPECT_FALSE(json); +} + +// This test verifies that the specific JSON element can be retrieved. +TEST_F(PostHttpRequestJsonTest, getJsonElement) { + setContextBasics("POST", "/isc/org", std::make_pair(1, 0)); + addHeaderToContext("Content-Length", json_body_.length()); + addHeaderToContext("Content-Type", "application/json"); + setBody(); + + ASSERT_NO_THROW(request_.finalize()); + + ConstElementPtr element; + ASSERT_NO_THROW(element = request_.getJsonElement("service")); + ASSERT_TRUE(element); + EXPECT_EQ("dhcp4", element->stringValue()); + + // An attempt to retrieve non-existing element should return NULL. + EXPECT_FALSE(request_.getJsonElement("bar")); +} + +} diff --git a/src/lib/http/tests/post_request_unittests.cc b/src/lib/http/tests/post_request_unittests.cc new file mode 100644 index 0000000000..c5f6427bc5 --- /dev/null +++ b/src/lib/http/tests/post_request_unittests.cc @@ -0,0 +1,72 @@ +// Copyright (C) 2016 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 <http/post_request.h> +#include <http/tests/request_test.h> +#include <gtest/gtest.h> + +using namespace isc::http; +using namespace isc::http::test; + +namespace { + +/// @brief Test fixutre class for @ref PostHttpRequest. +typedef HttpRequestTestBase<PostHttpRequest> PostHttpRequestTest; + +// This test verifies that PostHttpRequest class only accepts POST +// messages. +TEST_F(PostHttpRequestTest, requirePost) { + // Use a GET method that is not supported. + setContextBasics("GET", "/isc/org", std::make_pair(1, 0)); + addHeaderToContext("Content-Length", json_body_.length()); + addHeaderToContext("Content-Type", "application/json"); + + ASSERT_THROW(request_.create(), HttpRequestError); + + // Now use POST. It should be accepted. + setContextBasics("POST", "/isc/org", std::make_pair(1, 0)); + addHeaderToContext("Content-Length", json_body_.length()); + addHeaderToContext("Content-Type", "application/json"); + + EXPECT_NO_THROW(request_.create()); +} + +// This test verifies that PostHttpRequest requires "Content-Length" +// header. +TEST_F(PostHttpRequestTest, requireContentType) { + // No "Content-Type". It should fail. + setContextBasics("POST", "/isc/org", std::make_pair(1, 0)); + addHeaderToContext("Content-Length", json_body_.length()); + + ASSERT_THROW(request_.create(), HttpRequestError); + + // There is "Content-Type". It should pass. + setContextBasics("POST", "/isc/org", std::make_pair(1, 0)); + addHeaderToContext("Content-Length", json_body_.length()); + addHeaderToContext("Content-Type", "text/html"); + + EXPECT_NO_THROW(request_.create()); + +} + +// This test verifies that PostHttpRequest requires "Content-Type" +// header. +TEST_F(PostHttpRequestTest, requireContentLength) { + // No "Content-Length". It should fail. + setContextBasics("POST", "/isc/org", std::make_pair(1, 0)); + addHeaderToContext("Content-Type", "text/html"); + + ASSERT_THROW(request_.create(), HttpRequestError); + + // There is "Content-Length". It should pass. + setContextBasics("POST", "/isc/org", std::make_pair(1, 0)); + addHeaderToContext("Content-Length", json_body_.length()); + addHeaderToContext("Content-Type", "application/json"); +} + +} 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..a544d5fc09 --- /dev/null +++ b/src/lib/http/tests/request_parser_unittests.cc @@ -0,0 +1,286 @@ +// Copyright (C) 2016 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/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 (auto i = 0; i < http_req.size(); i += http_req.size() / 10) { + auto done = false; + // Get the size of the data chunk. + auto 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().first); + EXPECT_EQ(0, request.getHttpVersion().second); + + // 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 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().first); + EXPECT_EQ(1, request_.getHttpVersion().second); +} + +// 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().first); + EXPECT_EQ(1, request_.getHttpVersion().second); +} + +// 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().first); + EXPECT_EQ(1, request_.getHttpVersion().second); +} + +// 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().first); + EXPECT_EQ(0, request_.getHttpVersion().second); +} + +// 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().first); + EXPECT_EQ(0, request_.getHttpVersion().second); +} + +// 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().first); + EXPECT_EQ(0, request_.getHttpVersion().second); +} + +// 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); +} + +} diff --git a/src/lib/http/tests/request_test.h b/src/lib/http/tests/request_test.h new file mode 100644 index 0000000000..017de3c1ec --- /dev/null +++ b/src/lib/http/tests/request_test.h @@ -0,0 +1,83 @@ +// Copyright (C) 2016 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/. + +#ifndef HTTP_REQUEST_TEST_H +#define HTTP_REQUEST_TEST_H + +#include <http/request.h> +#include <boost/lexical_cast.hpp> +#include <gtest/gtest.h> +#include <string> +#include <utility> + +namespace isc { +namespace http { +namespace test { + +/// @brief Base test fixture class for testing @ref HttpRequest class and its +/// derivations. +/// +/// @tparam HttpRequestType Class under test. +template<typename HttpRequestType> +class HttpRequestTestBase : public ::testing::Test { +public: + + /// @brief Constructor. + /// + /// Creates HTTP request to be used in unit tests. + HttpRequestTestBase() + : request_() { + } + + /// @brief Destructor. + /// + /// Does nothing. + virtual ~HttpRequestTestBase() { + } + + /// @brief Initializes HTTP request context with basic information. + /// + /// It sets: + /// - HTTP method, + /// - URI, + /// - HTTP version number. + /// + /// @param method HTTP method as string. + /// @param uri URI. + /// @param version A pair of values of which the first is the major HTTP + /// version and the second is the minor HTTP version. + void setContextBasics(const std::string& method, const std::string& uri, + const std::pair<unsigned int, unsigned int>& version) { + request_.context()->method_ = method; + request_.context()->uri_ = uri; + request_.context()->http_version_major_ = version.first; + request_.context()->http_version_minor_ = version.second; + } + + /// @brief Adds HTTP header to the context. + /// + /// @param header_name HTTP header name. + /// @param header_value HTTP header value. This value will be converted to + /// a string using @c boost::lexical_cast. + /// @tparam ValueType Header value type. + template<typename ValueType> + void addHeaderToContext(const std::string& header_name, + const ValueType& header_value) { + request_.context()->headers_.push_back(HttpHeaderContext()); + request_.context()->headers_.back().name_ = header_name; + request_.context()->headers_.back().value_ = + boost::lexical_cast<std::string>(header_value); + } + + /// @brief Instance of the @ref HttpRequest or its derivation. + HttpRequestType request_; +}; + +} // namespace test +} // namespace http +} // namespace isc + +#endif diff --git a/src/lib/http/tests/request_unittests.cc b/src/lib/http/tests/request_unittests.cc new file mode 100644 index 0000000000..a668b8efc9 --- /dev/null +++ b/src/lib/http/tests/request_unittests.cc @@ -0,0 +1,157 @@ +// Copyright (C) 2016 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 <http/request.h> +#include <http/tests/request_test.h> +#include <boost/lexical_cast.hpp> +#include <gtest/gtest.h> +#include <utility> + +using namespace isc::http; +using namespace isc::http::test; + +namespace { + +typedef HttpRequestTestBase<HttpRequest> HttpRequestTest; + +TEST_F(HttpRequestTest, minimal) { + setContextBasics("GET", "/isc/org", std::make_pair(1, 1)); + ASSERT_NO_THROW(request_.create()); + + EXPECT_EQ(HttpRequest::Method::HTTP_GET, request_.getMethod()); + EXPECT_EQ("/isc/org", request_.getUri()); + EXPECT_EQ(1, request_.getHttpVersion().first); + EXPECT_EQ(1, request_.getHttpVersion().second); + + EXPECT_THROW(request_.getHeaderValue("Content-Length"), + HttpRequestNonExistingHeader); +} + +TEST_F(HttpRequestTest, includeHeaders) { + setContextBasics("POST", "/isc/org", std::make_pair(1, 0)); + addHeaderToContext("Content-Length", "1024"); + addHeaderToContext("Content-Type", "application/json"); + ASSERT_NO_THROW(request_.create()); + + EXPECT_EQ(HttpRequest::Method::HTTP_POST, request_.getMethod()); + EXPECT_EQ("/isc/org", request_.getUri()); + EXPECT_EQ(1, request_.getHttpVersion().first); + EXPECT_EQ(0, request_.getHttpVersion().second); + + std::string content_type; + ASSERT_NO_THROW(content_type = request_.getHeaderValue("Content-Type")); + EXPECT_EQ("application/json", content_type); + + uint64_t content_length; + ASSERT_NO_THROW( + content_length = request_.getHeaderValueAsUint64("Content-Length") + ); + EXPECT_EQ(1024, content_length); +} + +TEST_F(HttpRequestTest, requiredMethods) { + request_.requireHttpMethod(HttpRequest::Method::HTTP_GET); + request_.requireHttpMethod(HttpRequest::Method::HTTP_POST); + + setContextBasics("GET", "/isc/org", std::make_pair(1, 1)); + + ASSERT_NO_THROW(request_.create()); + + request_.context()->method_ = "POST"; + ASSERT_NO_THROW(request_.create()); + + request_.context()->method_ = "PUT"; + EXPECT_THROW(request_.create(), HttpRequestError); +} + +TEST_F(HttpRequestTest, requiredHttpVersion) { + request_.requireHttpVersion(std::make_pair(1, 0)); + request_.requireHttpVersion(std::make_pair(1, 1)); + + setContextBasics("POST", "/isc/org", std::make_pair(1, 0)); + EXPECT_NO_THROW(request_.create()); + + setContextBasics("POST", "/isc/org", std::make_pair(1, 1)); + EXPECT_NO_THROW(request_.create()); + + setContextBasics("POST", "/isc/org", std::make_pair(2, 0)); + EXPECT_THROW(request_.create(), HttpRequestError); +} + +TEST_F(HttpRequestTest, requiredHeader) { + request_.requireHeader("Content-Length"); + setContextBasics("POST", "/isc/org", std::make_pair(1, 0)); + + ASSERT_THROW(request_.create(), HttpRequestError); + + addHeaderToContext("Content-Type", "application/json"); + ASSERT_THROW(request_.create(), HttpRequestError); + + addHeaderToContext("Content-Length", "2048"); + EXPECT_NO_THROW(request_.create()); +} + +TEST_F(HttpRequestTest, requiredHeaderValue) { + request_.requireHeaderValue("Content-Type", "application/json"); + setContextBasics("POST", "/isc/org", std::make_pair(1, 0)); + addHeaderToContext("Content-Type", "text/html"); + + ASSERT_THROW(request_.create(), HttpRequestError); + + addHeaderToContext("Content-Type", "application/json"); + + EXPECT_NO_THROW(request_.create()); +} + +TEST_F(HttpRequestTest, notCreated) { + setContextBasics("POST", "/isc/org", std::make_pair(1, 0)); + addHeaderToContext("Content-Type", "text/html"); + addHeaderToContext("Content-Length", "1024"); + + EXPECT_THROW(static_cast<void>(request_.getMethod()), HttpRequestError); + EXPECT_THROW(static_cast<void>(request_.getHttpVersion()), + HttpRequestError); + EXPECT_THROW(static_cast<void>(request_.getUri()), HttpRequestError); + EXPECT_THROW(static_cast<void>(request_.getHeaderValue("Content-Type")), + HttpRequestError); + EXPECT_THROW(static_cast<void>(request_.getHeaderValueAsUint64("Content-Length")), + HttpRequestError); + EXPECT_THROW(static_cast<void>(request_.getBody()), HttpRequestError); + + ASSERT_NO_THROW(request_.finalize()); + + EXPECT_NO_THROW(static_cast<void>(request_.getMethod())); + EXPECT_NO_THROW(static_cast<void>(request_.getHttpVersion())); + EXPECT_NO_THROW(static_cast<void>(request_.getUri())); + EXPECT_NO_THROW(static_cast<void>(request_.getHeaderValue("Content-Type"))); + EXPECT_NO_THROW( + static_cast<void>(request_.getHeaderValueAsUint64("Content-Length")) + ); + EXPECT_NO_THROW(static_cast<void>(request_.getBody())); +} + +TEST_F(HttpRequestTest, getBody) { + std::string json_body = "{ \"param1\": \"foo\" }"; + + setContextBasics("POST", "/isc/org", std::make_pair(1, 0)); + addHeaderToContext("Content-Length", json_body.length()); + + request_.context()->body_ = json_body; + + ASSERT_NO_THROW(request_.finalize()); + + EXPECT_EQ(json_body, request_.getBody()); +} + +TEST_F(HttpRequestTest, requiresBody) { + ASSERT_FALSE(request_.requiresBody()); + request_.requireHeader("Content-Length"); + EXPECT_TRUE(request_.requiresBody()); +} + +} |