Commit 0fc1359e authored by Marcin Siodelski's avatar Marcin Siodelski

[5077] Implemented HTTP request parser.

parent f7c734bb
......@@ -781,6 +781,7 @@ INPUT = ../src/bin/d2 \
../src/lib/eval \
../src/lib/exceptions \
../src/lib/hooks \
../src/lib/http \
../src/lib/log \
../src/lib/log/compiler \
../src/lib/log/interprocess \
......
......@@ -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)
......
// 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
......@@ -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");
......
// 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
// 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
// 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
// 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
// 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");
}
}
}
}
// 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.
///