summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorMarcin Siodelski <marcin@isc.org>2016-12-08 14:15:28 +0100
committerMarcin Siodelski <marcin@isc.org>2016-12-13 13:52:08 +0100
commit0fc1359eba5c653c9ec5d4ad3d2940741287b994 (patch)
tree9b54850dda9601c1652deb9caa94648b50f21585 /src
parent[5077] Created libkea-http library. (diff)
downloadkea-0fc1359eba5c653c9ec5d4ad3d2940741287b994.tar.xz
kea-0fc1359eba5c653c9ec5d4ad3d2940741287b994.zip
[5077] Implemented HTTP request parser.
Diffstat (limited to 'src')
-rw-r--r--src/lib/http/Makefile.am7
-rw-r--r--src/lib/http/header_context.h23
-rw-r--r--src/lib/http/http_log.cc2
-rw-r--r--src/lib/http/post_request.cc20
-rw-r--r--src/lib/http/post_request.h31
-rw-r--r--src/lib/http/post_request_json.cc75
-rw-r--r--src/lib/http/post_request_json.h76
-rw-r--r--src/lib/http/request.cc259
-rw-r--r--src/lib/http/request.h277
-rw-r--r--src/lib/http/request_context.h44
-rw-r--r--src/lib/http/request_parser.cc677
-rw-r--r--src/lib/http/request_parser.h450
-rw-r--r--src/lib/http/tests/Makefile.am7
-rw-r--r--src/lib/http/tests/post_request_json_unittests.cc172
-rw-r--r--src/lib/http/tests/post_request_unittests.cc72
-rw-r--r--src/lib/http/tests/request_parser_unittests.cc286
-rw-r--r--src/lib/http/tests/request_test.h83
-rw-r--r--src/lib/http/tests/request_unittests.cc157
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());
+}
+
+}