From: Marcin Siodelski Date: Thu, 22 Dec 2016 13:51:48 +0000 (+0100) Subject: [5088] Proper documentation of the new HTTP classes. X-Git-Tag: trac5090_base~2^2~9 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=8e70bf1ee84b028885be6a8a7a19b0313a6fc2ad;p=thirdparty%2Fkea.git [5088] Proper documentation of the new HTTP classes. Also, included a couple of modifications in the API. --- diff --git a/src/lib/http/date_time.cc b/src/lib/http/date_time.cc index 91e1bbb0b9..0800b67ae6 100644 --- a/src/lib/http/date_time.cc +++ b/src/lib/http/date_time.cc @@ -5,13 +5,10 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/. #include -#include -#include #include #include #include -using namespace boost::gregorian; using namespace boost::local_time; using namespace boost::posix_time; @@ -57,6 +54,14 @@ HttpDateTime::fromRfc850(const std::string& time_string) { HttpDateTime HttpDateTime::fromAsctime(const std::string& time_string) { + // The asctime() puts space instead of leading 0 in days of + // month. The %e # formatter of time_input_facet doesn't deal + // with this. To deal with this, we make a copy of the string + // holding formatted time and replace a space preceding day + // number with 0. Thanks to this workaround we can use the + // %d formatter which seems to work fine. This has a side + // effect of accepting timestamps such as Sun Nov 06 08:49:37 1994, + // but it should be ok to be liberal in this case. std::string time_string_copy(time_string); boost::replace_all(time_string_copy, " ", " 0"); return (HttpDateTime(fromString(time_string_copy, @@ -68,20 +73,25 @@ HttpDateTime::fromAsctime(const std::string& time_string) { HttpDateTime HttpDateTime::fromAny(const std::string& time_string) { HttpDateTime date_time; + // Try to parse as a timestamp specified in RFC 1123 format. try { date_time = fromRfc1123(time_string); return (date_time); } catch (...) { + // Ignore errors, simply try different format. ; } + // Try to parse as a timestamp specified in RFC 850 format. try { date_time = fromRfc850(time_string); return (date_time); } catch (...) { + // Ignore errors, simply try different format. ; } + // Try to parse as a timestamp output by asctime() function. try { date_time = fromAsctime(time_string); } catch (...) { @@ -98,8 +108,12 @@ std::string HttpDateTime::toString(const std::string& format, const std::string& method_name) const { std::ostringstream s; + // Create raw pointer. The output stream will take responsibility for + // deleting the object. time_facet* df(new time_facet(format.c_str())); s.imbue(std::locale(s.getloc(), df)); + + // Convert time value to a string. s << time_; if (s.fail()) { isc_throw(HttpTimeConversionError, "unable to convert " @@ -116,14 +130,20 @@ HttpDateTime::fromString(const std::string& time_string, const std::string& method_name, const bool zone_check) { std::istringstream s(time_string); + // Create raw pointer. The input stream will take responsibility for + // deleting the object. time_input_facet* tif(new time_input_facet(format)); s.imbue(std::locale(s.getloc(), tif)); time_zone_ptr zone(new posix_time_zone("GMT")); local_date_time ldt = local_microsec_clock::local_time(zone); + + // Parse the time value. The stream will not automatically detect whether + // the zone is GMT. We need to check it on our own. s >> ldt; if (s.fail() || - (zone_check && (!ldt.zone() || ldt.zone()->std_zone_abbrev() != "GMT"))) { + (zone_check && (!ldt.zone() || + ldt.zone()->std_zone_abbrev() != "GMT"))) { isc_throw(HttpTimeConversionError, "unable to parse " << method_name << " time value of '" << time_string << "'"); diff --git a/src/lib/http/date_time.h b/src/lib/http/date_time.h index e6970b310f..c26a381875 100644 --- a/src/lib/http/date_time.h +++ b/src/lib/http/date_time.h @@ -14,46 +14,142 @@ namespace isc { namespace http { +/// @brief Exception thrown when there is an error during time conversion. +/// +/// The most common reason for this exception is that the unsupported time +/// format was used as an input to the time parsing functions. class HttpTimeConversionError : public Exception { public: HttpTimeConversionError(const char* file, size_t line, const char* what) : isc::Exception(file, line, what) { }; }; +/// @brief This class parses and generates time values used in HTTP. +/// +/// The HTTP protocol have historically allowed 3 different date/time formats +/// (see https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html). These are: +/// - Sun, 06 Nov 1994 08:49:37 GMT +/// - Sunday, 06-Nov-94 08:49:37 GMT +/// - Sun Nov 6 08:49:37 1994 +/// +/// The first format is preferred but implementations must also support +/// remaining two obsolete formats for compatibility. This class implements +/// parsers and generators for all three formats. It uses @c boost::posix_time +/// to represent time and date. It uses @ref boost::date_time::time_facet +/// and @ref boost::date_time::time_input_facet to generate and parse the +/// timestamps. class HttpDateTime { public: + /// @brief Default constructor. + /// + /// Sets current universal time as time value. HttpDateTime(); + /// @brief Construct from @ref boost::posix_time::ptime object. + /// + /// @param t time value to be set. explicit HttpDateTime(const boost::posix_time::ptime& t); + /// @brief Returns time encapsulated by this class. + /// + /// @return @ref boost::posix_time::ptime value encapsulated by the instance + /// of this class. boost::posix_time::ptime getPtime() const { return (time_); } + /// @brief Returns time value formatted as specified in RFC 1123. + /// + /// @return A string containing time value formatted as + /// Sun, 06 Nov 1994 08:49:37 GMT. std::string rfc1123Format() const; + /// @brief Returns time value formatted as specified in RFC 850. + /// + /// @return A string containing time value formatted as + /// Sunday, 06-Nov-94 08:49:37 GMT. std::string rfc850Format() const; + /// @brief Returns time value formatted as output of ANSI C's + /// asctime(). + /// + /// @return A string containing time value formatted as + /// Sun Nov 6 08:49:37 1994. std::string asctimeFormat() const; + /// @brief Creates an instance from a string containing time value + /// formatted as specified in RFC 1123. + /// + /// @param time_string Input string holding formatted time value. + /// @return Instance of @ref HttpDateTime. + /// @throw HttpTimeConversionError if provided timestamp has invalid + /// format. static HttpDateTime fromRfc1123(const std::string& time_string); + /// @brief Creates an instance from a string containing time value + /// formatted as specified in RFC 850. + /// + /// @param time_string Input string holding formatted time value. + /// @return Instance of @ref HttpDateTime. + /// @throw HttpTimeConversionError if provided timestamp has invalid + /// format. static HttpDateTime fromRfc850(const std::string& time_string); + /// @brief Creates an instance from a string containing time value + /// formatted as output from asctime() function. + /// + /// @param time_string Input string holding formatted time value. + /// @return Instance of @ref HttpDateTime. + /// @throw HttpTimeConversionError if provided timestamp has invalid + /// format. static HttpDateTime fromAsctime(const std::string& time_string); + /// @brief Creates an instance from a string containing time value + /// formatted in one of the supported formats. + /// + /// This method will detect the format of the time value and parse it. + /// It tries parsing the value in the following order: + /// - a format specified in RFC 1123, + /// - a format specified in RFC 850, + /// - a format of asctime output. + /// + /// @param time_string Input string holding formatted time value. + /// @return Instance of @ref HttpDateTime. + /// @throw HttpTimeConversionError if provided value doesn't match any + /// of the supported formats. static HttpDateTime fromAny(const std::string& time_string); private: + /// @brief Generic method formatting a time value to a specified format. + //// + /// @param format Time format as accepted by the + /// @c boost::date_time::time_facet. std::string toString(const std::string& format, const std::string& method_name) const; + /// @brief Generic method parsing time value and converting it to the + /// instance of @c boost::posix_time::ptime. + /// + /// @param time_string Input string holding formatted time value. + /// @param format Time format as accepted by the + /// @c boost::date_time::time_input_facet. + /// @param method_name Name of the expected format to appear in the error + /// message if parsing fails, e.g. RFC 1123, RFC 850 or asctime. + /// @param zone_check Indicates if the time zone name should be validated + /// during parsing. This should be set to false for the formats which + /// lack time zones (e.g. asctime). + /// + /// @return Instance of the @ref boost::posix_time::ptime created from the + /// input string. + /// @throw HttpTimeConversionError if provided value doesn't match the + /// specified format. static boost::posix_time::ptime fromString(const std::string& time_string, const std::string& format, const std::string& method_name, const bool zone_check = true); + /// @brief Time value encapsulated by this class instance. boost::posix_time::ptime time_; }; diff --git a/src/lib/http/response.cc b/src/lib/http/response.cc index 1b492cface..705b5fe79d 100644 --- a/src/lib/http/response.cc +++ b/src/lib/http/response.cc @@ -4,6 +4,7 @@ // 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 #include #include #include @@ -14,6 +15,7 @@ using namespace isc::http; namespace { +/// @brief A map of status codes to status names. const std::map status_code_to_description = { { HttpStatusCode::OK, "OK" }, { HttpStatusCode::CREATED, "Created" }, @@ -33,6 +35,7 @@ const std::map status_code_to_description = { { HttpStatusCode::SERVICE_UNAVAILABLE, "Service Unavailable" } }; +/// @brief New line (CRLF). const std::string crlf = "\r\n"; } @@ -41,10 +44,15 @@ namespace isc { namespace http { HttpResponse::HttpResponse(const HttpVersion& version, - const HttpStatusCode& status_code) + const HttpStatusCode& status_code, + const CallSetGenericBody& generic_body) : http_version_(version), status_code_(status_code), headers_(), body_() { - setGenericBody(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. + setGenericBody(status_code); + } } void @@ -54,12 +62,14 @@ HttpResponse::setBody(const std::string& body) { bool HttpResponse::isClientError(const HttpStatusCode& status_code) { + // Client errors have status codes of 4XX. uint16_t c = statusCodeToNumber(status_code); return ((c >= 400) && (c < 500)); } bool HttpResponse::isServerError(const HttpStatusCode& status_code) { + // Server errors have status codes of 5XX. uint16_t c = statusCodeToNumber(status_code); return ((c >= 500) && (c < 600)); } @@ -82,35 +92,43 @@ HttpResponse::statusCodeToNumber(const HttpStatusCode& status_code) { std::string HttpResponse::getDateHeaderValue() const { - local_date_time t(local_sec_clock::local_time(time_zone_ptr())); - std::stringstream s; - local_time_facet* lf(new local_time_facet("%a, %d %b %Y %H:%M:%S GMT")); - s.imbue(std::locale(s.getloc(), lf)); - s << t; - - return (s.str()); + // This returns current time in the recommended format. + HttpDateTime date_time; + return (date_time.rfc1123Format()); } std::string HttpResponse::toString() const { std::ostringstream s; + // HTTP version number and status code. s << "HTTP/" << http_version_.first << "." << http_version_.second; s << " " << static_cast(status_code_); s << " " << statusCodeToString(status_code_) << crlf; - for (auto header = headers_.cbegin(); header != headers_.cend(); - ++header) { - s << header->first << ": " << header->second << crlf; - } + // 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 headers(headers_); - s << "Date: " << getDateHeaderValue() << crlf; + // Update or add "Date" header. + addHeaderInternal("Date", getDateHeaderValue(), headers); + // Add "Content-Length" if body present. if (!body_.empty()) { - s << "Content-Length: " << body_.length() << crlf; + addHeaderInternal("Content-Length", body_.length(), headers); + } + + // Include all headers. + for (auto header = headers.cbegin(); header != headers.cend(); + ++header) { + s << header->first << ": " << header->second << crlf; } s << crlf; + // Include message body. s << body_; return (s.str()); diff --git a/src/lib/http/response.h b/src/lib/http/response.h index a8302d0672..0a0aae79c2 100644 --- a/src/lib/http/response.h +++ b/src/lib/http/response.h @@ -25,6 +25,7 @@ public: isc::Exception(file, line, what) { }; }; +/// @brief HTTP status codes. enum class HttpStatusCode : std::uint16_t { OK = 200, CREATED = 201, @@ -44,6 +45,34 @@ enum class HttpStatusCode : std::uint16_t { SERVICE_UNAVAILABLE = 503 }; +/// @brief Encapsulates the boolean value indicating if the @ref HttpResponse +/// constructor should call its @c setGenericBody method during construction. +struct CallSetGenericBody { + + /// @brief Contrsuctor. + /// + /// @param set A boolean value indicating if the method should be called + /// or not. + explicit CallSetGenericBody(const bool set) + : set_(set) { + } + + /// @brief Returns encapsulated true. + static const CallSetGenericBody& yes() { + static CallSetGenericBody yes(true); + return (yes); + } + + /// @brief Returns encapsulated false. + static const CallSetGenericBody& no() { + static CallSetGenericBody no(false); + return (no); + } + + /// @brief A storage for the boolean flag. + bool set_; +}; + class HttpResponse; /// @brief Pointer to the @ref HttpResponse object. @@ -52,43 +81,153 @@ typedef boost::shared_ptr HttpResponsePtr; /// @brief Pointer to the const @ref HttpResponse object. typedef boost::shared_ptr 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. +/// +/// It contains @c toString method generating a textual representation of +/// the HTTP response, which is send to the client over TCP socket. class HttpResponse { public: - HttpResponse(const HttpVersion& version, const HttpStatusCode& status_code); - + /// @brief Constructor. + /// + /// Creates basic instance of the object. It sets the HTTP version and the + /// status code to be included in the response. + /// + /// @param version HTTP version. + /// @param status_code HTTP status code. + /// @param generic_body Indicates if the constructor should call + /// @c setGenericBody to create a generic content for the given + /// status code. This should be set to "no" when the constructor is + /// called by the derived class which provides its own implementation + /// of the @c setGenericBody method. + explicit HttpResponse(const HttpVersion& version, + const HttpStatusCode& status_code, + const CallSetGenericBody& generic_body = + CallSetGenericBody::yes()); + + /// @brief Destructor. + /// + /// A class having virtual methods must have a virtual destructor. virtual ~HttpResponse() { } - void addHeader(const std::string& name, const std::string& value) { - headers_[name] = value; + /// @brief Adds HTTP header to the response. + /// + /// The "Content-Length" and "Date" headers should not be added using this + /// method because they are generated and added automatically when the + /// @c toString is called. + /// + /// @param name Header name. + /// @param value Header value. + /// @tparam ValueType Type of the header value. + template + void addHeader(const std::string& name, const ValueType& value) { + addHeaderInternal(name, value, headers_); } - virtual void setBody(const std::string& body); + /// @brief Assigns body/content to the message. + /// + /// @param body Body to be assigned. + void setBody(const std::string& body); + /// @brief Checks if the status code indicates client error. + /// + /// @param status_code HTTP status code. + /// @return true if the status code indicates client error. static bool isClientError(const HttpStatusCode& status_code); + /// @brief Checks if the status code indicates server error. + /// + /// @param status_code HTTP status code. + /// @return true if the status code indicates server error. static bool isServerError(const HttpStatusCode& status_code); + /// @brief Converts status code to string. + /// + /// @param status_code HTTP status code. + /// @return Textual representation of the status code. static std::string statusCodeToString(const HttpStatusCode& status_code); - std::string toString() const; + /// @brief Returns textual representation of the HTTP response. + /// + /// It includes the "Date" header with the current time in RFC 1123 format. + /// It also includes "Content-Length" when the response has a non-empty + /// body. + /// + /// @return Textual representation of the HTTP response. + std::string toString() const ; protected: + /// @brief Adds HTTP header to the map. + /// + /// @param name Header name. + /// @param value Header value. + /// @param [out] headers A map to which header value should be inserted. + /// @tparam ValueType Type of the header value. + template + void addHeaderInternal(const std::string& name, const ValueType& value, + std::map& headers) const { + try { + headers[name] = boost::lexical_cast(value); + + } catch (const boost::bad_lexical_cast& ex) { + isc_throw(HttpResponseError, "unable to convert the " + << name << " header value to a string"); + } + } + + /// @brief Returns current time formatted as required by RFC 1123. + /// + /// This method is virtual so as it can be overriden in unit tests + /// to return a "predictable" value of time, e.g. constant value. + /// + /// @return Current time formatted as required by RFC 1123. virtual std::string getDateHeaderValue() const; + /// @brief Convenience method converting status code to numeric value. + /// + /// @param status_code Status code represented as enum. + /// @return Numeric representation of the status code. static uint16_t statusCodeToNumber(const HttpStatusCode& status_code); private: + /// @brief Sets generic body for the given status code. + /// + /// Most of the classes derived from @ref HttpResponse will expect + /// a certain content type. Depending on the content type used they + /// will use different body formats for error messages. For example, + /// a response using text/html will use HTML within the response + /// body. The application/json will use JSON body etc. There is a + /// need to implement class specific way of generating the body + /// for error messages. Thus, each derivation of this class is + /// required to implement class specific @ref setGenericBody function + /// which should be called in the class constructor. + /// + /// This is also the case for this class, though the implementation + /// of @c setGenericBody is currently no-op. + /// + /// Note that this class can't be declared virtual because it is + /// meant to be called from the class constructor. + /// + /// @param status_code Status code for which the body should be + /// generated. void setGenericBody(const HttpStatusCode& status_code) { }; + /// @brief Holds HTTP version for the response. HttpVersion http_version_; + /// @brief Holds status code for the response. HttpStatusCode status_code_; + /// @brief Holds HTTP headers for the response. std::map headers_; + /// @brief Holds the body/content. std::string body_; }; diff --git a/src/lib/http/response_creator.cc b/src/lib/http/response_creator.cc index fbf9fd8b51..ddedbaaa0d 100644 --- a/src/lib/http/response_creator.cc +++ b/src/lib/http/response_creator.cc @@ -11,14 +11,19 @@ namespace http { HttpResponsePtr HttpResponseCreator::createHttpResponse(const ConstHttpRequestPtr& request) { + // This should never happen. This method must only be called with a + // non null request, so we consider it unlikely internal server error. if (!request) { isc_throw(HttpResponseError, "internal server error: HTTP request is null"); } + // If not finalized, the request parsing failed. Generate HTTP 400. if (!request->isFinalized()) { return (createStockBadRequest(request)); } + // Message has been successfully parsed. Create implementation specific + // response to this request. return (createDynamicHttpResponse(request)); } diff --git a/src/lib/http/response_creator.h b/src/lib/http/response_creator.h index 9284232ad1..e83bc9e767 100644 --- a/src/lib/http/response_creator.h +++ b/src/lib/http/response_creator.h @@ -13,18 +13,71 @@ namespace isc { namespace http { +/// @brief Specifies an interface for classes creating HTTP responses +/// from HTTP requests. +/// +/// HTTP is designed to carry various types of the content. Most commonly +/// this is text/html. In Kea, the application/json content type is used +/// to carry control commands in JSON format. The libkea-http library is +/// meant to be generic and provide means for transferring different types +/// of content, depending on the use case. +/// +/// This class specifies a common interface for generating HTTP responses +/// from HTTP requests using specific content type and being used in some +/// specific context. Kea modules providing HTTP services need to +/// implement their specific derivations of the @ref HttpResponseCreator +/// class. These derivations use classes derived from @ref HttpRequest as +/// an input and classes derived from @ref HttpResponse as an output of +/// @c createHttpResponse method. class HttpResponseCreator { public: + /// @brief Destructor. + /// + /// Classes with virtual functions need virtual destructors. virtual ~HttpResponseCreator() { }; + /// @brief Create HTTP response from HTTP request received. + /// + /// This class implements a generic logic for creating a HTTP response. + /// Derived classes do not override this method. They merely implement + /// the methods it calls. + /// + /// The request processing may generally fail at one of the two stages: + /// parsing or interpretation of the parsed request. During the former + /// stage the request's syntax is checked, i.e. HTTP version, URI, + /// headers etc. During the latter stage the HTTP server checks if the + /// request is valid within the specific context, e.g. valid HTTP version + /// used, expected content type etc. + /// + /// In the @ref HttpRequest terms, the request has gone through the + /// first stage if it is "finalized" (see @ref HttpRequest::finalize). + /// This method accepts instances of both finalized and not finalized + /// requests. If the request isn't finalized it indicates that + /// the request parsing has failed. In such case, this method calls + /// @c createStockBadRequest to generate a response with HTTP 400 status + /// code. If the request is finalized, this method calls + /// @c createDynamicHttpResponse to generate implementation specific + /// response to the received request. + /// + /// @param request Pointer to an object representing HTTP request. + /// @return Pointer to the object encapsulating generated HTTP response. + /// @throw HttpResponseError if request is a NULL pointer. HttpResponsePtr createHttpResponse(const ConstHttpRequestPtr& request); protected: + /// @brief Creates implentation specific HTTP 400 response.. + /// + /// @param request Pointer to an object representing HTTP request. + /// @return Pointer to an object representing HTTP 400 response. virtual HttpResponsePtr createStockBadRequest(const ConstHttpRequestPtr& request) const = 0; + /// @brief Creates implementation specific HTTP response. + /// + /// @param request Pointer to an object representing HTTP request. + /// @return Pointer to an object representing HTTP response. virtual HttpResponsePtr createDynamicHttpResponse(const ConstHttpRequestPtr& request) = 0; diff --git a/src/lib/http/response_json.cc b/src/lib/http/response_json.cc index 77d4f15519..7df4348d85 100644 --- a/src/lib/http/response_json.cc +++ b/src/lib/http/response_json.cc @@ -12,14 +12,24 @@ namespace isc { namespace http { HttpResponseJson::HttpResponseJson(const HttpVersion& version, - const HttpStatusCode& status_code) - : HttpResponse(version, status_code) { + const HttpStatusCode& status_code, + const CallSetGenericBody& generic_body) + : HttpResponse(version, status_code, CallSetGenericBody::no()) { addHeader("Content-Type", "application/json"); - setGenericBody(status_code); + // This class provides its own implementation of the setGenericBody. + // We call it here unless the derived class calls this constructor + // from its own constructor and indicates that we shouldn't set the + // generic content in the body. + if (generic_body.set_) { + setGenericBody(status_code); + } } void HttpResponseJson::setGenericBody(const HttpStatusCode& status_code) { + // Only generate the content for the client or server errors. For + // other status codes (status OK in particular) the content should + // be created using setBodyAsJson or setBody. if (isClientError(status_code) || isServerError(status_code)) { std::map map_elements; map_elements["result"] = diff --git a/src/lib/http/response_json.h b/src/lib/http/response_json.h index cf79d3e437..83ac0b54ba 100644 --- a/src/lib/http/response_json.h +++ b/src/lib/http/response_json.h @@ -13,19 +13,52 @@ namespace isc { namespace http { +class HttpResponseJson; + +/// @brief Pointer to the @ref HttpResponseJson object. +typedef boost::shared_ptr HttpResponseJsonPtr; + +/// @brief Represents HTTP response with JSON content. +/// +/// This is a specialization of the @ref HttpResponse class which +/// includes "Content-Type" equal to "application/json". It also provides +/// methods to create JSON content within HTTP responses. class HttpResponseJson : public HttpResponse { public: - HttpResponseJson(const HttpVersion& version, - const HttpStatusCode& status_code); - - virtual void setGenericBody(const HttpStatusCode& status_code); + /// @brief Constructor. + /// + /// @param version HTTP version. + /// @param status_code HTTP status code. + /// @param generic_body Indicates if the constructor should call + /// @c setGenericBody to create a generic content for the given + /// status code. This should be set to "no" when the constructor is + /// called by the derived class which provides its own implementation + /// of the @c setGenericBody method. + explicit HttpResponseJson(const HttpVersion& version, + const HttpStatusCode& status_code, + const CallSetGenericBody& generic_body = + CallSetGenericBody::yes()); + /// @brief Generates JSON content from the data structures represented + /// as @ref data::ConstElementPtr. + /// + /// @param json_body A data structure representing JSON content. virtual void setBodyAsJson(const data::ConstElementPtr& json_body); -}; +private: -typedef boost::shared_ptr HttpResponseJsonPtr; + /// @brief Sets generic body for the given status code. + /// + /// This method generates JSON content for the HTTP client and server + /// errors. The generated JSON structure is a map containing "result" + /// value holding HTTP status code (e.g. 400) and the "text" string + /// holding a status code description. + /// + /// @param status_code Status code for which the body should be + /// generated. + void setGenericBody(const HttpStatusCode& status_code); +}; } } diff --git a/src/lib/http/tests/date_time_unittests.cc b/src/lib/http/tests/date_time_unittests.cc index af8186dcdb..e563ddbaa7 100644 --- a/src/lib/http/tests/date_time_unittests.cc +++ b/src/lib/http/tests/date_time_unittests.cc @@ -8,9 +8,6 @@ #include #include #include -#include - - #include using namespace boost::gregorian; @@ -19,9 +16,21 @@ using namespace isc::http; namespace { +/// @brief Test fixture class for @ref HttpDateTime. class HttpDateTimeTest : public ::testing::Test { public: + /// @brief Checks time value against expected values. + /// + /// This method uses value of @ref date_time_ for the test. + /// + /// @param exp_day_of_week Expected day of week. + /// @param exp_day Expected day of month. + /// @param exp_month Expected month. + /// @param exp_year Expected year. + /// @param exp_hours Expected hour value. + /// @param exp_minutes Expected minutes value. + /// @param exp_seconds Expected seconds value. void testDateTime(const unsigned short exp_day_of_week, const unsigned short exp_day, const unsigned short exp_month, @@ -29,31 +38,40 @@ public: const long exp_hours, const long exp_minutes, const long exp_seconds) { + // Retrieve @c boost::posix_time::ptime value. ptime as_ptime = date_time_.getPtime(); + // Date is contained within this object. date date_part = as_ptime.date(); + // Verify weekday. greg_weekday day_of_week = date_part.day_of_week(); EXPECT_EQ(exp_day_of_week, day_of_week.as_number()); + // Verify day of month. greg_day day = date_part.day(); EXPECT_EQ(exp_day, day.as_number()); + // Verify month. greg_month month = date_part.month(); EXPECT_EQ(exp_month, month.as_number()); + // Verify year. greg_year year = date_part.year(); EXPECT_EQ(exp_year, static_cast(year)); + // Retrieve time of the day and verify hour, minute and second. time_duration time_of_day = as_ptime.time_of_day(); EXPECT_EQ(exp_hours, time_of_day.hours()); EXPECT_EQ(exp_minutes, time_of_day.minutes()); EXPECT_EQ(exp_seconds, time_of_day.seconds()); } + /// @brief Date/time value which should be set by the tests. HttpDateTime date_time_; }; +// Test formatting as specified in RFC 1123. TEST_F(HttpDateTimeTest, rfc1123Format) { date gdate(greg_year(2002), greg_month(1), greg_day(20)); time_duration tm(23, 59, 59, 0); @@ -64,6 +82,7 @@ TEST_F(HttpDateTimeTest, rfc1123Format) { EXPECT_EQ("Sun, 20 Jan 2002 23:59:59 GMT", formatted); } +// Test formatting as specified in RFC 850. TEST_F(HttpDateTimeTest, rfc850Format) { date gdate(greg_year(1994), greg_month(8), greg_day(6)); time_duration tm(11, 12, 13, 0); @@ -75,6 +94,7 @@ TEST_F(HttpDateTimeTest, rfc850Format) { EXPECT_EQ("Saturday, 06-Aug-94 11:12:13 GMT", formatted); } +// Test formatting as output of asctime(). TEST_F(HttpDateTimeTest, asctimeFormat) { date gdate(greg_year(1999), greg_month(11), greg_day(2)); time_duration tm(03, 57, 12, 0); @@ -86,6 +106,7 @@ TEST_F(HttpDateTimeTest, asctimeFormat) { EXPECT_EQ("Tue Nov 2 03:57:12 1999", formatted); } +// Test parsing time in RFC 1123 format. TEST_F(HttpDateTimeTest, fromRfc1123) { ASSERT_NO_THROW( date_time_ = HttpDateTime::fromRfc1123("Wed, 21 Dec 2016 18:53:45 GMT") @@ -103,6 +124,7 @@ TEST_F(HttpDateTimeTest, fromRfc1123) { HttpTimeConversionError); } +// Test parsing time in RFC 850 format. TEST_F(HttpDateTimeTest, fromRfc850) { ASSERT_NO_THROW( date_time_ = HttpDateTime::fromRfc850("Wednesday, 21-Dec-16 18:53:45 GMT"); @@ -118,6 +140,7 @@ TEST_F(HttpDateTimeTest, fromRfc850) { HttpTimeConversionError); } +// Test parsing time in asctime() format. TEST_F(HttpDateTimeTest, fromRfcAsctime) { ASSERT_NO_THROW( date_time_ = HttpDateTime::fromAsctime("Wed Dec 21 08:49:37 2016"); @@ -133,6 +156,7 @@ TEST_F(HttpDateTimeTest, fromRfcAsctime) { HttpTimeConversionError); } +// Test parsing time in RFC 1123 format using HttpDateTime::fromAny(). TEST_F(HttpDateTimeTest, fromAnyRfc1123) { ASSERT_NO_THROW( date_time_ = HttpDateTime::fromAny("Thu, 05 Jan 2017 09:15:06 GMT"); @@ -140,6 +164,7 @@ TEST_F(HttpDateTimeTest, fromAnyRfc1123) { testDateTime(4, 5, 1, 2017, 9, 15, 06); } +// Test parsing time in RFC 850 format using HttpDateTime::fromAny(). TEST_F(HttpDateTimeTest, fromAnyRfc850) { ASSERT_NO_THROW( date_time_ = HttpDateTime::fromAny("Saturday, 18-Feb-17 01:02:10 GMT"); @@ -147,6 +172,7 @@ TEST_F(HttpDateTimeTest, fromAnyRfc850) { testDateTime(6, 18, 2, 2017, 1, 2, 10); } +// Test parsing time in asctime() format using HttpDateTime::fromAny(). TEST_F(HttpDateTimeTest, fromAnyAsctime) { ASSERT_NO_THROW( date_time_ = HttpDateTime::fromAny("Wed Mar 1 15:45:07 2017 GMT"); @@ -154,6 +180,8 @@ TEST_F(HttpDateTimeTest, fromAnyAsctime) { testDateTime(3, 1, 3, 2017, 15, 45, 7); } +// Test that HttpDateTime::fromAny throws exception if unsupported format is +// used. TEST_F(HttpDateTimeTest, fromAnyInvalidFormat) { EXPECT_THROW(HttpDateTime::fromAsctime("20020131T235959"), HttpTimeConversionError); diff --git a/src/lib/http/tests/response_creator_unittests.cc b/src/lib/http/tests/response_creator_unittests.cc index 72400a8f8b..be9d25a0c1 100644 --- a/src/lib/http/tests/response_creator_unittests.cc +++ b/src/lib/http/tests/response_creator_unittests.cc @@ -18,45 +18,77 @@ using namespace isc::http::test; namespace { +/// @brief Test HTTP response. typedef TestHttpResponseBase Response; + +/// @brief Pointer to test HTTP response. typedef boost::shared_ptr ResponsePtr; +/// @brief Implementation of the @ref HttpResponseCreator. class TestHttpResponseCreator : public HttpResponseCreator { private: + /// @brief Creates HTTP 400 response. + /// + /// @param request Pointer to the HTTP request. + /// @return Pointer to the generated HTTP 400 response. virtual HttpResponsePtr createStockBadRequest(const ConstHttpRequestPtr& request) const { + // The request hasn't been finalized so the request object + // doesn't contain any information about the HTTP version number + // used. But, the context should have this data (assuming the + // HTTP version is parsed ok). HttpVersion http_version(request->context()->http_version_major_, request->context()->http_version_minor_); + // This will generate the response holding JSON content. ResponsePtr response(new Response(http_version, HttpStatusCode::BAD_REQUEST)); return (response); } + /// @brief Creates HTTP response. + /// + /// @param request Pointer to the HTTP request. + /// @return Pointer to the generated HTTP OK response with no content. virtual HttpResponsePtr createDynamicHttpResponse(const ConstHttpRequestPtr& request) { + // The simplest thing is to create a response with no content. + // We don't need content to test our class. ResponsePtr response(new Response(request->getHttpVersion(), HttpStatusCode::OK)); return (response); } }; +// This test verifies that Bad Request status is generated when the request +// hasn't been finalized. TEST(HttpResponseCreatorTest, badRequest) { HttpResponsePtr response; + // Create a request but do not finalize it. HttpRequestPtr request(new HttpRequest()); + request->context()->http_version_major_ = 1; + request->context()->http_version_minor_ = 0; + request->context()->method_ = "GET"; + request->context()->uri_ = "/foo"; + + // Use test specific implementation of Response Creator. It should + // generate HTTP error 400. TestHttpResponseCreator creator; ASSERT_NO_THROW(response = creator.createHttpResponse(request)); ASSERT_TRUE(response); - EXPECT_EQ("HTTP/0.0 400 Bad Request\r\n" + EXPECT_EQ("HTTP/1.0 400 Bad Request\r\n" + "Content-Length: 40\r\n" "Content-Type: application/json\r\n" - "Date: Tue, 19 Dec 2016 18:53:35 GMT\r\n" - "Content-Length: 40\r\n\r\n" + "Date: Tue, 19 Dec 2016 18:53:35 GMT\r\n\r\n" "{ \"result\": 400, \"text\": \"Bad Request\" }", response->toString()); } +// This test verifies that response is generated successfully from the +// finalized/parsed request. TEST(HttpResponseCreatorTest, goodRequest) { HttpResponsePtr response; + // Create request and finalize it. HttpRequestPtr request(new HttpRequest()); request->context()->http_version_major_ = 1; request->context()->http_version_minor_ = 0; @@ -64,6 +96,8 @@ TEST(HttpResponseCreatorTest, goodRequest) { request->context()->uri_ = "/foo"; ASSERT_NO_THROW(request->finalize()); + // Use test specific implementation of the Response Creator to generate + // a response. TestHttpResponseCreator creator; ASSERT_NO_THROW(response = creator.createHttpResponse(request)); ASSERT_TRUE(response); diff --git a/src/lib/http/tests/response_json_unittests.cc b/src/lib/http/tests/response_json_unittests.cc index 6bde0be15d..589702fa91 100644 --- a/src/lib/http/tests/response_json_unittests.cc +++ b/src/lib/http/tests/response_json_unittests.cc @@ -17,11 +17,21 @@ using namespace isc::http::test; namespace { +/// @brief Response type used in tests. typedef TestHttpResponseBase TestHttpResponseJson; +/// @brief Test fixture class for @ref HttpResponseJson. class HttpResponseJsonTest : public ::testing::Test { public: + /// @brief Constructor. + /// + /// Initializes the following class members: + /// - json_string_ - which is a pretty formatted JSON content, + /// - json_ - A structure of Element objects representing the JSON, + /// - json_string_from_json_ - which is a JSON text converted back from + /// the json_ data structure. It is the same content as json_string_ + /// but has different whitespaces. HttpResponseJsonTest() : json_(), json_string_(), json_string_from_json_() { json_string_ = @@ -42,10 +52,16 @@ public: json_string_from_json_ = json_->str(); } + /// @brief Test that the response format is correct. + /// + /// @param status_code HTTP status code for which the response should + /// be tested. + /// @param status_message HTTP status message. void testGenericResponse(const HttpStatusCode& status_code, const std::string& status_message) { TestHttpResponseJson response(HttpVersion(1, 0), status_code); std::ostringstream status_message_json; + // Build the expected content. status_message_json << "{ \"result\": " << static_cast(status_code) << ", \"text\": " @@ -53,32 +69,40 @@ public: std::ostringstream response_string; response_string << "HTTP/1.0 " << static_cast(status_code) << " " - << status_message << "\r\n" - "Content-Type: application/json\r\n" - "Date: " << response.getDateHeaderValue() << "\r\n"; + << status_message << "\r\n"; + // The content must only be generated for error codes. if (HttpResponse::isClientError(status_code) || HttpResponse::isServerError(status_code)) { response_string << "Content-Length: " << status_message_json.str().size() << "\r\n"; } - response_string << "\r\n"; + // Content-Type and Date are automatically included. + response_string << "Content-Type: application/json\r\n" + "Date: " << response.getDateHeaderValue() << "\r\n\r\n"; if (HttpResponse::isClientError(status_code) || HttpResponse::isServerError(status_code)) { response_string << status_message_json.str(); } + // Check that the output is as expected. EXPECT_EQ(response_string.str(), response.toString()); } + /// @brief JSON content represented as structure of Element objects. ConstElementPtr json_; + + /// @brief Pretty formatted JSON content. std::string json_string_; + + /// @brief JSON content parsed back from json_ structure. std::string json_string_from_json_; }; +// Test that the response with custom JSON content is generated properly. TEST_F(HttpResponseJsonTest, responseWithContent) { TestHttpResponseJson response(HttpVersion(1, 1), HttpStatusCode::OK); ASSERT_NO_THROW(response.setBodyAsJson(json_)); @@ -86,13 +110,14 @@ TEST_F(HttpResponseJsonTest, responseWithContent) { std::ostringstream response_string; response_string << "HTTP/1.1 200 OK\r\n" + "Content-Length: " << json_string_from_json_.length() << "\r\n" "Content-Type: application/json\r\n" - "Date: " << response.getDateHeaderValue() << "\r\n" - "Content-Length: " << json_string_from_json_.length() - << "\r\n\r\n" << json_string_from_json_; + "Date: " << response.getDateHeaderValue() << "\r\n\r\n" + << json_string_from_json_; EXPECT_EQ(response_string.str(), response.toString()); } +// Test that generic responses are created properly. TEST_F(HttpResponseJsonTest, genericResponse) { testGenericResponse(HttpStatusCode::OK, "OK"); testGenericResponse(HttpStatusCode::CREATED, "Created"); diff --git a/src/lib/http/tests/response_unittests.cc b/src/lib/http/tests/response_unittests.cc index 9da562b9a4..2af95e6c96 100644 --- a/src/lib/http/tests/response_unittests.cc +++ b/src/lib/http/tests/response_unittests.cc @@ -19,13 +19,22 @@ using namespace isc::http::test; namespace { +/// @brief Response type used in tests. typedef TestHttpResponseBase TestHttpResponse; +/// @brief Test fixture class for @ref HttpResponse. class HttpResponseTest : public ::testing::Test { public: + /// @brief Checks if the format of the response is correct. + /// + /// @param status_code HTTP status code in the response. + /// @param status_message HTTP status message in the response. void testResponse(const HttpStatusCode& status_code, const std::string& status_message) { + // Create the response. Because we're using derived class + // it returns the fixed value of the Date header, which is + // very useful in unit tests. TestHttpResponse response(HttpVersion(1, 0), status_code); response.addHeader("Content-Type", "text/html"); std::ostringstream response_string; @@ -36,32 +45,38 @@ public: EXPECT_EQ(response_string.str(), response.toString()); } - }; +// Test the case of HTTP OK message. TEST_F(HttpResponseTest, responseOK) { + // Include HTML body. const std::string sample_body = "" "Kea page title" "

Some header

" ""; + // Create the message and add some headers. TestHttpResponse response(HttpVersion(1, 0), HttpStatusCode::OK); response.addHeader("Content-Type", "text/html"); response.addHeader("Host", "kea.example.org"); response.setBody(sample_body); + // Create a string holding expected response. Note that the Date + // is a fixed value returned by the customized TestHttpResponse + // classs. std::ostringstream response_string; response_string << "HTTP/1.0 200 OK\r\n" + "Content-Length: " << sample_body.length() << "\r\n" "Content-Type: text/html\r\n" - "Host: kea.example.org\r\n" "Date: " << response.getDateHeaderValue() << "\r\n" - "Content-Length: " << sample_body.length() - << "\r\n\r\n" << sample_body; + "Host: kea.example.org\r\n\r\n" << sample_body; + EXPECT_EQ(response_string.str(), response.toString()); } +// Test generic responses for various status codes. TEST_F(HttpResponseTest, genericResponse) { testResponse(HttpStatusCode::OK, "OK"); testResponse(HttpStatusCode::CREATED, "Created"); @@ -81,6 +96,7 @@ TEST_F(HttpResponseTest, genericResponse) { testResponse(HttpStatusCode::SERVICE_UNAVAILABLE, "Service Unavailable"); } +// Test if the class correctly identifies client errors. TEST_F(HttpResponseTest, isClientError) { EXPECT_FALSE(HttpResponse::isClientError(HttpStatusCode::OK)); EXPECT_FALSE(HttpResponse::isClientError(HttpStatusCode::CREATED)); @@ -100,6 +116,7 @@ TEST_F(HttpResponseTest, isClientError) { EXPECT_FALSE(HttpResponse::isClientError(HttpStatusCode::SERVICE_UNAVAILABLE)); } +// Test if the class correctly identifies server errors. TEST_F(HttpResponseTest, isServerError) { EXPECT_FALSE(HttpResponse::isServerError(HttpStatusCode::OK)); EXPECT_FALSE(HttpResponse::isServerError(HttpStatusCode::CREATED)); @@ -119,10 +136,23 @@ TEST_F(HttpResponseTest, isServerError) { EXPECT_TRUE(HttpResponse::isServerError(HttpStatusCode::SERVICE_UNAVAILABLE)); } +// Test that the generated time value, being included in the Date +// header, is correct. TEST_F(HttpResponseTest, getDateHeaderValue) { + // Create a response and retrieve the value to be included in the + // Date header. This value should hold a current time in the + // RFC1123 format. TestHttpResponse response(HttpVersion(1, 0), HttpStatusCode::OK); std::string generated_date = response.generateDateHeaderValue(); + + // Use our date/time utilities to parse this value into the ptime. HttpDateTime parsed_time = HttpDateTime::fromRfc1123(generated_date); + + // Now that we have it converted back, we can check how far this + // value is from the current time. To be on the safe side, we check + // that it is not later than 10 seconds apart, rather than checking + // it for equality. In fact, checking it for equality would almost + // certainly cause an error. Especially on a virtual machine. time_duration parsed_to_current = microsec_clock::universal_time() - parsed_time.getPtime(); EXPECT_LT(parsed_to_current.seconds(), 10);