Commit e6fe7f50 authored by Marcin Siodelski's avatar Marcin Siodelski

[5451] Restructure HTTP requests/response to support inbound and outbound.

parent 12bf626c
......@@ -40,6 +40,7 @@ createStockHttpResponse(const ConstHttpRequestPtr& request,
}
// This will generate the response holding JSON content.
HttpResponsePtr response(new HttpResponseJson(http_version, status_code));
response->finalize();
return (response);
}
......@@ -75,6 +76,7 @@ createDynamicHttpResponse(const ConstHttpRequestPtr& request) {
HttpResponseJsonPtr http_response = boost::dynamic_pointer_cast<
HttpResponseJson>(createStockHttpResponse(request, HttpStatusCode::OK));
http_response->setBodyAsJson(response);
http_response->finalize();
return (http_response);
}
......
......@@ -7,6 +7,8 @@
#ifndef HTTP_HEADER_CONTEXT_H
#define HTTP_HEADER_CONTEXT_H
#include <boost/lexical_cast.hpp>
#include <cstdint>
#include <string>
namespace isc {
......@@ -31,6 +33,14 @@ struct HttpHeaderContext {
HttpHeaderContext(const std::string& name, const std::string& value)
: name_(name), value_(value) {
}
/// @brief Constructor.
///
/// @param name Header name.
/// @param value Numeric value for the header.
HttpHeaderContext(const std::string& name, const int64_t value)
: name_(name), value_(boost::lexical_cast<std::string>(value)) {
}
};
} // namespace http
......
......@@ -9,10 +9,10 @@
namespace isc {
namespace http {
HttpMessage::HttpMessage()
: required_versions_(), http_version_(HttpVersion::HTTP_10()),
required_headers_(), created_(false), finalized_(false), headers_(),
body_() {
HttpMessage::HttpMessage(const HttpMessage::Direction& direction)
: direction_(direction), required_versions_(),
http_version_(HttpVersion::HTTP_10()), required_headers_(),
created_(false), finalized_(false), headers_() {
}
HttpMessage::~HttpMessage() {
......
......@@ -12,7 +12,7 @@
#include <http/http_types.h>
#include <map>
#include <set>
#include <stdint.h>
#include <cstdint>
#include <string>
namespace isc {
......@@ -36,15 +36,60 @@ public:
/// @brief Base class for @ref HttpRequest and @ref HttpResponse.
///
/// This abstract class provides a common functionality for the HTTP
/// requests and responses. Each such message can be marked as outbound
/// or inbound. An HTTP inbound request is the one received by the server
/// and HTTP inbound response is the response received by the client.
/// Conversely, an HTTP outbound request is the request created by the
/// client and HTTP outbound response is the response created by the
/// server. There are differences in how the inbound and outbound
/// messages are created. The inbound messages are received over the
/// TCP sockets and parsed by the parsers. The parsed information is
/// stored in a context, i.e. structure holding raw information and
/// associated with the given @c HttpMessage instance. Once the message
/// is parsed and all required information is stored in the context,
/// the @c create method is called to validate and fetch information
/// from the context into the message. The @c finalize method is called
/// to commit the HTTP message body into the message.
///
/// The outbound message is created locally from the known data, e.g.
/// HTTP version number, URI, method etc. The headers can be then
/// appended to the message via the context. In order to use this message
/// the @c finalize method must be called to commit this information.
/// Them, @c toString method can be called to generate the message in
/// the textual form, which can be transferred via TCP socket.
class HttpMessage {
public:
/// @brief Specifies the direction of the HTTP message.
enum Direction {
INBOUND,
OUTBOUND
};
/// @brief Constructor.
HttpMessage();
///
/// @param direction Direction of the message (inbound or outbound).
explicit HttpMessage(const Direction& direction);
/// @brief Destructor.
virtual ~HttpMessage();
/// @brief Returns HTTP message direction.
Direction getDirection() const {
return (direction_);
}
/// @brief Sets direction for the HTTP message.
///
/// This is mostly useful in unit testing.
///
/// @param direction New direction of the HTTP message.
void setDirection(const Direction& direction) {
direction_ = direction;
}
/// @brief Specifies HTTP version allowed.
///
/// Allowed HTTP versions must be specified prior to calling @ref create
......@@ -163,12 +208,6 @@ public:
return (finalized_);
}
/// @brief Checks if the message indicates persistent connection.
///
/// @return true if the message indicates persistent connection, false
/// otherwise.
virtual bool isPersistent() const = 0;
protected:
/// @brief Checks if the @ref create was called.
......@@ -190,7 +229,7 @@ protected:
///
/// @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.
/// @tparam T Element type, @ref HttpVersion etc.
///
/// @return true if the element set is empty or if the element belongs
/// to the set.
......@@ -200,6 +239,9 @@ protected:
return (element_set.empty() || element_set.count(element) > 0);
}
/// @brief Message direction (inbound or outbound).
Direction direction_;
/// @brief Set of required HTTP versions.
///
/// If the set is empty, all versions are allowed.
......@@ -228,10 +270,6 @@ protected:
/// @brief Parsed HTTP headers.
HttpHeaderMap headers_;
/// @brief HTTP body as string.
std::string body_;
};
} // end of namespace isc::http
......
// Copyright (C) 2016 Internet Systems Consortium, Inc. ("ISC")
// Copyright (C) 2016-2017 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
......
......@@ -29,7 +29,7 @@ typedef boost::shared_ptr<const PostHttpRequest> ConstPostHttpRequestPtr;
class PostHttpRequest : public HttpRequest {
public:
/// @brief Constructor.
/// @brief Constructor for inbound HTTP request.
PostHttpRequest();
};
......
......@@ -39,8 +39,8 @@ typedef boost::shared_ptr<const PostHttpRequestJson> ConstPostHttpRequestJsonPtr
class PostHttpRequestJson : public PostHttpRequest {
public:
/// @brief Constructor.
PostHttpRequestJson();
/// @brief Constructor for inbound HTTP request.
explicit PostHttpRequestJson();
/// @brief Complete parsing of the HTTP request.
///
......
......@@ -9,13 +9,31 @@
#include <boost/lexical_cast.hpp>
#include <sstream>
namespace {
/// @brief New line (CRLF).
const std::string crlf = "\r\n";
}
namespace isc {
namespace http {
HttpRequest::HttpRequest()
: HttpMessage(), required_methods_(),
: HttpMessage(INBOUND), required_methods_(),
method_(Method::HTTP_METHOD_UNKNOWN),
context_(new HttpRequestContext()) {
}
HttpRequest::HttpRequest(const Method& method, const std::string& uri,
const HttpVersion& version)
: HttpMessage(OUTBOUND), required_methods_(),
method_(Method::HTTP_METHOD_UNKNOWN),
context_(new HttpRequestContext()) {
context()->method_ = methodToString(method);
context()->uri_ = uri;
context()->http_version_major_ = version.major_;
context()->http_version_minor_ = version.minor_;
}
void
......@@ -56,7 +74,7 @@ HttpRequest::create() {
headers_[hdr->getLowerCaseName()] = hdr;
}
if (!context_->body_.empty() && (headers_.count("content-length") == 0)) {
if (getDirection() == HttpMessage::OUTBOUND) {
HttpHeaderPtr hdr(new HttpHeader("Content-Length",
boost::lexical_cast<std::string>(context_->body_.length())));
headers_["content-length"] = hdr;
......@@ -99,7 +117,6 @@ HttpRequest::finalize() {
// Copy the body from the context. Derive classes may further
// interpret the body contents, e.g. against the Content-Type.
body_ = context_->body_;
finalized_ = true;
}
......@@ -109,7 +126,6 @@ HttpRequest::reset() {
finalized_ = false;
method_ = HttpRequest::Method::HTTP_METHOD_UNKNOWN;
headers_.clear();
body_.clear();
}
HttpRequest::Method
......@@ -136,15 +152,15 @@ HttpRequest::toString() const {
std::ostringstream s;
s << methodToString(getMethod()) << " " << getUri() << " HTTP/" <<
getHttpVersion().major_ << "." << getHttpVersion().minor_ << "\r\n";
getHttpVersion().major_ << "." << getHttpVersion().minor_ << crlf;
for (auto header_it = headers_.cbegin(); header_it != headers_.cend();
++header_it) {
s << header_it->second->getName() << ": " << header_it->second->getValue()
<< "\r\n";
<< crlf;
}
s << "\r\n";
s << crlf;
s << getBody();
......@@ -153,6 +169,10 @@ HttpRequest::toString() const {
bool
HttpRequest::isPersistent() const {
if (getDirection() == OUTBOUND) {
isc_throw(InvalidOperation, "can't call isPersistent for the outbound request");
}
HttpHeaderPtr conn = getHeaderSafe("connection");
std::string conn_value;
if (conn) {
......
......@@ -10,7 +10,7 @@
#include <http/http_message.h>
#include <http/request_context.h>
#include <boost/shared_ptr.hpp>
#include <stdint.h>
#include <cstdint>
namespace isc {
namespace http {
......@@ -22,15 +22,6 @@ public:
HttpMessageError(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) { };
};
class HttpRequest;
/// @brief Pointer to the @ref HttpRequest object.
......@@ -41,18 +32,19 @@ typedef boost::shared_ptr<const HttpRequest> ConstHttpRequestPtr;
/// @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.
/// This derivation of the @c HttpMessage class is specialized to represent
/// HTTP requests. This class provides two constructors for creating an inbound
/// and outbound request instance respectively. This class is associated with
/// an instance of the @c HttpRequestContext, which is used to provide request
/// specific values, such as: HTTP method, version, URI and headers.
///
/// 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.
/// 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 @c PostHttpRequest class derives from @c HttpRequest
/// and it requires that request uses POST method. The @c PostHttpRequestJson,
/// which derives from @c PostHttpRequest requires that the POST message
/// includes body holding a JSON structure and provides methods to parse the
/// JSON body.
class HttpRequest : public HttpMessage {
public:
......@@ -68,16 +60,21 @@ public:
HTTP_METHOD_UNKNOWN
};
/// @brief Constructor.
///
/// Creates new context (@ref HttpRequestContext).
/// @brief Constructor for inbound HTTP request.
HttpRequest();
/// @brief Returns reference to the @ref HttpRequestContext.
/// @brief Constructor for oubtound HTTP request.
///
/// @param method HTTP method, e.g. POST.
/// @param uri URI.
/// @param version HTTP version.
HttpRequest(const Method& method, const std::string& uri, const HttpVersion& version);
/// @brief Returns pointer to the @ref HttpRequestContext.
///
/// The context holds intermediate data for creating a request. The request
/// parser stores parsed raw data in the context. When parsing is finished,
/// the data are validated and committed into the @ref HttpRequest.
/// the data are validated and committed into the @c HttpRequest.
///
/// @return Pointer to the underlying @ref HttpRequestContext.
const HttpRequestContextPtr& context() const {
......@@ -92,39 +89,22 @@ public:
/// @param method HTTP method allowed for the request.
void requireHttpMethod(const HttpRequest::Method& method);
/// @brief Reads parsed request from the @ref HttpRequestContext, validates
/// the request and stores parsed information.
/// @brief Commits information held in the context into the request.
///
/// 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.
/// This function reads HTTP method, version and headers from the context
/// and validates their values. For the outbound messages, it automatically
/// appends Content-Length header to the request, based on the length of the
/// 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 or create outbound 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.
/// @brief Completes creation of the HTTP request.
///
/// For the outbound (client) request, this method must be called after
/// setting all required values in the request context. The Content-Length
/// is generally not explicitly set by the caller in this case. This method
/// computes the value of the Content-Length and inserts the suitable header
/// when it finds non-empty 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.
/// This method marks the message as finalized. The outbound request may now be
/// sent over the TCP socket. The information from the inbound message may be
/// read, including the request body.
virtual void finalize();
/// @brief Reset the state of the object.
......@@ -140,13 +120,12 @@ public:
/// @brief Returns HTTP message body as string.
std::string getBody() const;
/// @brief Returns HTTP message as text.
/// @brief Returns HTTP message as string.
///
/// This method is called to generate the outbound HTTP message. Make
/// sure to call @c finalize prior to calling this method.
virtual std::string toString() const;
/// @brief Checks if the client has requested persistent connection.
///
/// For the HTTP/1.0 case, the connection is persistent if the client has
......@@ -156,6 +135,7 @@ public:
///
/// @return true if the client has requested persistent connection, false
/// otherwise.
/// @throw InvalidOperation if the method is called for the outbound message.
bool isPersistent() const;
protected:
......
......@@ -44,11 +44,18 @@ const std::string crlf = "\r\n";
namespace isc {
namespace http {
HttpResponse::HttpResponse()
: HttpMessage(INBOUND), context_(new HttpResponseContext()) {
}
HttpResponse::HttpResponse(const HttpVersion& version,
const HttpStatusCode& status_code,
const CallSetGenericBody& generic_body)
: http_version_(version), status_code_(status_code), headers_(),
body_(), context_(new HttpResponseContext()) {
: HttpMessage(OUTBOUND), context_(new HttpResponseContext()) {
context_->http_version_major_ = version.major_;
context_->http_version_minor_ = version.minor_;
context_->status_code_ = static_cast<uint16_t>(status_code);
if (generic_body.set_) {
// This currently does nothing, but it is useful to have it here as
// an example how to implement it in the derived classes.
......@@ -58,11 +65,90 @@ HttpResponse::HttpResponse(const HttpVersion& version,
void
HttpResponse::create() {
try {
http_version_.major_ = context_->http_version_major_;
http_version_.minor_ = context_->http_version_minor_;
// Check if the HTTP version is allowed for this request.
if (!inRequiredSet(http_version_, required_versions_)) {
isc_throw(BadValue, "use of HTTP version "
<< http_version_.major_ << "."
<< http_version_.minor_
<< " not allowed");
}
// Copy headers from the context.
for (auto header = context_->headers_.begin();
header != context_->headers_.end();
++header) {
HttpHeaderPtr hdr(new HttpHeader(header->name_, header->value_));
headers_[hdr->getLowerCaseName()] = hdr;
}
if (getDirection() == HttpMessage::OUTBOUND) {
HttpHeaderPtr length_header(new HttpHeader("Content-Length", boost::lexical_cast<std::string>
(context_->body_.length())));
headers_["content-length"] = length_header;
HttpHeaderPtr date_header(new HttpHeader("Date", getDateHeaderValue()));;
headers_["date"] = date_header;
}
// Iterate over required headers and check that they exist
// in the HTTP response.
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 " << req_header->first
<< " not found in the HTTP response");
} else if (!req_header->second->getValue().empty() &&
!header->second->isValueEqual(req_header->second->getValue())) {
// If specific value is required for the header, check
// that the value in the HTTP response matches it.
isc_throw(BadValue, "required header's " << header->first
<< " value is " << req_header->second->getValue()
<< ", but " << header->second->getValue() << " was found");
}
}
} catch (const std::exception& ex) {
// Reset the state of the object if we failed at any point.
reset();
isc_throw(HttpResponseError, ex.what());
}
// All ok.
created_ = true;
}
void
HttpResponse::finalize() {
if (!created_) {
create();
}
finalized_ = true;
}
void
HttpResponse::setBody(const std::string& body) {
body_ = body;
HttpResponse::reset() {
created_ = false;
finalized_ = false;
headers_.clear();
}
HttpStatusCode
HttpResponse::getStatusCode() const {
checkCreated();
return (static_cast<HttpStatusCode>(context_->status_code_));
}
std::string
HttpResponse::getBody() const {
checkFinalized();
return (context_->body_);
}
bool
......@@ -104,43 +190,33 @@ HttpResponse::getDateHeaderValue() const {
std::string
HttpResponse::toBriefString() const {
checkFinalized();
std::ostringstream s;
// HTTP version number and status code.
s << "HTTP/" << http_version_.major_ << "." << http_version_.minor_;
s << " " << static_cast<uint16_t>(status_code_);
s << " " << statusCodeToString(status_code_) << crlf;
s << " " << context_->status_code_;
s << " " << statusCodeToString(static_cast<HttpStatusCode>(context_->status_code_)) << crlf;
return (s.str());
}
std::string
HttpResponse::toString() const {
std::ostringstream s;
// HTTP version number and status code.
s << toBriefString();
// We need to at least insert "Date" header into the HTTP headers. This
// method is const thus we can't insert it into the headers_ map. We'll
// work on the copy of the map. Admittedly, we could just append "Date"
// into the generated string but we prefer that headers are ordered
// alphabetically.
std::map<std::string, std::string> headers(headers_);
// Update or add "Date" header.
addHeaderInternal("Date", getDateHeaderValue(), headers);
// Always add "Content-Length", perhaps equal to 0.
addHeaderInternal("Content-Length", body_.length(), headers);
// Include all headers.
for (auto header = headers.cbegin(); header != headers.cend();
++header) {
s << header->first << ": " << header->second << crlf;
for (auto header_it = headers_.cbegin(); header_it != headers_.cend();
++header_it) {
s << header_it->second->getName() << ": " << header_it->second->getValue()
<< crlf;
}
s << crlf;
// Include message body.
s << body_;
s << getBody();
return (s.str());
}
......
......@@ -7,23 +7,21 @@
#ifndef HTTP_RESPONSE_H
#define HTTP_RESPONSE_H
#include <exceptions/exceptions.h>
#include <http/http_types.h>
#include <http/header_context.h>
#include <http/http_message.h>
#include <http/response_context.h>
#include <boost/lexical_cast.hpp>
#include <boost/shared_ptr.hpp>
#include <cstdint>
#include <map>
#include <string>
#include <vector>
namespace isc {
namespace http {
/// @brief Generic exception thrown by @ref HttpResponse class.
class HttpResponseError : public Exception {
class HttpResponseError : public HttpMessageError {
public:
HttpResponseError(const char* file, size_t line, const char* what) :
isc::Exception(file, line, what) { };
HttpMessageError(file, line, what) { };
};
/// @brief HTTP status codes (cf RFC 2068)
......@@ -85,16 +83,24 @@ typedef boost::shared_ptr<const HttpResponse> ConstHttpResponsePtr;
/// @brief Represents HTTP response message.
///
/// This class represents HTTP response message. An instance of this object
/// or its derivation is typically created by the implementation of the
/// @ref HttpResponseCreator interface.
/// This derivation of the @c HttpMessage class is specialized to represent
/// HTTP responses. This class provides two constructors for creating an inbound
/// and outbound response instance respectively. This class is associated with
/// an instance of the @c HttpResponseContext, which is used to provide response
/// specific values, such as HTTP status and headers.
///
/// It contains @c toString method generating a textual representation of
/// the HTTP response, which is send to the client over TCP socket.
class HttpResponse {
/// The derivations of this class provide specializations and specify the HTTP
/// versions and headers supported/required in the specific use cases. For example,
/// the @c HttpResponseJson class derives from the @c HttpResponse and it requires
/// that response includes a body in the JSON format.
class HttpResponse : public HttpMessage {
public:
/// @brief Constructor.
/// @brief Constructor for the inbound HTTP response.