From: Marcin Siodelski Date: Mon, 11 Dec 2017 20:55:46 +0000 (+0100) Subject: [5448] HTTP headers are case insensitive. X-Git-Tag: trac5452_base~5^2~3 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=528ab52132c73844f1204c73394682e0317d4346;p=thirdparty%2Fkea.git [5448] HTTP headers are case insensitive. --- diff --git a/src/lib/http/Makefile.am b/src/lib/http/Makefile.am index 2b0eed5752..599711500c 100644 --- a/src/lib/http/Makefile.am +++ b/src/lib/http/Makefile.am @@ -28,6 +28,7 @@ libkea_http_la_SOURCES += date_time.cc date_time.h libkea_http_la_SOURCES += http_log.cc http_log.h libkea_http_la_SOURCES += header_context.h libkea_http_la_SOURCES += http_acceptor.h +libkea_http_la_SOURCES += http_header.cc http_header.h libkea_http_la_SOURCES += http_types.h libkea_http_la_SOURCES += listener.cc listener.h libkea_http_la_SOURCES += post_request.cc post_request.h diff --git a/src/lib/http/http_header.cc b/src/lib/http/http_header.cc new file mode 100644 index 0000000000..3e62cc14a8 --- /dev/null +++ b/src/lib/http/http_header.cc @@ -0,0 +1,54 @@ +// Copyright (C) 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 +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include +#include +#include +#include + +namespace isc { +namespace http { + +HttpHeader::HttpHeader(const std::string& header_name, + const std::string& header_value) + : header_name_(header_name), header_value_(header_value) { +} + +uint64_t +HttpHeader::getUint64Value() const { + try { + return (boost::lexical_cast(header_value_)); + + } catch (const boost::bad_lexical_cast& ex) { + isc_throw(BadValue, header_name_ << " HTTP header value " + << header_value_ << " is not a valid number"); + } +} + +std::string +HttpHeader::getLowerCaseName() const { + std::string ln = header_name_; + util::str::lowercase(ln); + return (ln); +} + +std::string +HttpHeader::getLowerCaseValue() const { + std::string lc = header_value_; + util::str::lowercase(lc); + return (lc); +} + +bool +HttpHeader::isValueEqual(const std::string& v) const { + std::string lcv = v; + util::str::lowercase(lcv); + return (lcv == getLowerCaseValue()); +} + + +} // end of namespace isc::http +} // end of namespace isc diff --git a/src/lib/http/http_header.h b/src/lib/http/http_header.h new file mode 100644 index 0000000000..975ad35efe --- /dev/null +++ b/src/lib/http/http_header.h @@ -0,0 +1,70 @@ +// Copyright (C) 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 +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef HTTP_HEADER_H +#define HTTP_HEADER_H + +#include +#include + +namespace isc { +namespace http { + +/// @brief Represents HTTP header including a header name and value. +/// +/// It includes methods for retrieving header name and value in lower case +/// and for case insensitive comparison of header values. +class HttpHeader { +public: + + /// @brief Constructor. + /// + /// @param header_name Header name. + /// @param header_value Header value. + explicit HttpHeader(const std::string& header_name, + const std::string& header_value = ""); + + /// @brief Returns header name. + std::string getName() const { + return (header_name_); + } + + /// @brief Returns header value. + std::string getValue() const { + return (header_value_); + } + + /// @brief Returns header value as unsigned integer. + /// + /// @throw BadValue if the header value is not a valid number. + uint64_t getUint64Value() const; + + /// @brief Returns lower case header name. + std::string getLowerCaseName() const; + + /// @brief Returns lower case header value. + std::string getLowerCaseValue() const; + + /// @brief Case insensitive comparison of header value. + /// + /// @param v Value to be compared. + /// + /// @return true if header value is equal, false otherwise. + bool isValueEqual(const std::string& v) const; + +private: + + std::string header_name_; ///< Header name. + std::string header_value_; ///< Header value. +}; + +/// @brief Pointer to the @c HttpHeader class. +typedef boost::shared_ptr HttpHeaderPtr; + +} // end of namespace isc::http +} // end of namespace isc + +#endif // HTTP_HEADER_H diff --git a/src/lib/http/request.cc b/src/lib/http/request.cc index 206ab332fe..b54a35ad4d 100644 --- a/src/lib/http/request.cc +++ b/src/lib/http/request.cc @@ -34,13 +34,15 @@ 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] = ""; + HttpHeaderPtr hdr(new HttpHeader(header_name)); + required_headers_[hdr->getLowerCaseName()] = hdr; } void HttpRequest::requireHeaderValue(const std::string& header_name, const std::string& header_value) { - required_headers_[header_name] = header_value; + HttpHeaderPtr hdr(new HttpHeader(header_name, header_value)); + required_headers_[hdr->getLowerCaseName()] = hdr; } bool @@ -48,7 +50,9 @@ 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()); + // Use lower case header name because this is how it is indexed in + // the storage. + return (required_headers_.find("content-length") != required_headers_.end()); } void @@ -79,7 +83,8 @@ HttpRequest::create() { for (auto header = context_->headers_.begin(); header != context_->headers_.end(); ++header) { - headers_[header->name_] = header->value_; + HttpHeaderPtr hdr(new HttpHeader(header->name_, header->value_)); + headers_[hdr->getLowerCaseName()] = hdr; } // Iterate over required headers and check that they exist @@ -91,13 +96,13 @@ HttpRequest::create() { if (header == headers_.end()) { isc_throw(BadValue, "required header " << req_header->first << " not found in the HTTP request"); - } else if (!req_header->second.empty() && - header->second != req_header->second) { + } 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 request matches it. isc_throw(BadValue, "required header's " << header->first - << " value is " << req_header->second - << ", but " << header->second << " was found"); + << " value is " << req_header->second->getValue() + << ", but " << header->second->getValue() << " was found"); } } @@ -151,31 +156,34 @@ HttpRequest::getHttpVersion() const { context_->http_version_minor_)); } -std::string -HttpRequest::getHeaderValue(const std::string& header) const { +HttpHeaderPtr +HttpRequest::getHeader(const std::string& header_name) const { checkCreated(); - auto header_it = headers_.find(header); + HttpHeader hdr(header_name); + auto header_it = headers_.find(hdr.getLowerCaseName()); if (header_it != headers_.end()) { return (header_it->second); } + // No such header. - isc_throw(HttpRequestNonExistingHeader, header << " HTTP header" + isc_throw(HttpRequestNonExistingHeader, header_name << " 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); +std::string +HttpRequest::getHeaderValue(const std::string& header_name) const { + return (getHeader(header_name)->getValue()); +} +uint64_t +HttpRequest::getHeaderValueAsUint64(const std::string& header_name) const { try { - return (boost::lexical_cast(header_value)); + return (getHeader(header_name)->getUint64Value()); - } catch (const boost::bad_lexical_cast& ex) { + } catch (const std::exception& 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"); + isc_throw(HttpRequestError, ex.what()); } } @@ -185,6 +193,11 @@ HttpRequest::getBody() const { return (context_->body_); } +bool +HttpRequest::isPersistent() const { + return (false); +} + void HttpRequest::checkCreated() const { if (!created_) { diff --git a/src/lib/http/request.h b/src/lib/http/request.h index 07cd29c7e3..cb7178efdc 100644 --- a/src/lib/http/request.h +++ b/src/lib/http/request.h @@ -8,6 +8,7 @@ #define HTTP_REQUEST_H #include +#include #include #include #include @@ -179,20 +180,25 @@ public: /// @brief Returns HTTP version number (major and minor). HttpVersion getHttpVersion() const; + /// @brief Returns object encapsulating HTTP header. + /// + /// @param header_name HTTP header name. + HttpHeaderPtr getHeader(const std::string& header_name) const; + /// @brief Returns a value of the specified HTTP header. /// - /// @param header Name of the HTTP header. + /// @param header_name Name of the HTTP header. /// /// @throw HttpRequestError if the header doesn't exist. - std::string getHeaderValue(const std::string& header) const; + std::string getHeaderValue(const std::string& header_name) const; /// @brief Returns a value of the specified HTTP header as number. /// - /// @param header Name of the HTTP header. + /// @param header_name 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; + uint64_t getHeaderValueAsUint64(const std::string& header_name) const; /// @brief Returns HTTP message body as string. std::string getBody() const; @@ -207,6 +213,17 @@ public: return (finalized_); } + /// @brief Checks if the client has requested persistent connection. + /// + /// For the HTTP/1.0 case, the connection is persistent if the client has + /// included Connection: keep-alive header. For the HTTP/1.1 case, the + /// connection is assumed to be persistent unless Connection: close header + /// has been included. + /// + /// @return true if the client has requested persistent connection, false + /// otherwise. + bool isPersistent() const; + //@} protected: @@ -264,14 +281,17 @@ protected: /// If the set is empty, all versions are allowed. std::set required_versions_; + /// @brief Map of HTTP headers indexed by lower case header names. + typedef std::map HttpHeaderMap; + /// @brief Map holding required HTTP headers. /// - /// The key of this map specifies the HTTP header name. The value - /// specifies 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 required_headers_; + /// The key of this map specifies the lower case HTTP header name. + /// If the value of the HTTP header 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 (case + /// insensitive) to the value in the map. + HttpHeaderMap required_headers_; /// @brief Flag indicating whether @ref create was called. bool created_; @@ -283,7 +303,7 @@ protected: Method method_; /// @brief Parsed HTTP headers. - std::map headers_; + HttpHeaderMap headers_; /// @brief Pointer to the @ref HttpRequestContext holding parsed /// data. diff --git a/src/lib/http/tests/Makefile.am b/src/lib/http/tests/Makefile.am index d34853e118..a9c637deea 100644 --- a/src/lib/http/tests/Makefile.am +++ b/src/lib/http/tests/Makefile.am @@ -22,6 +22,7 @@ TESTS += libhttp_unittests libhttp_unittests_SOURCES = connection_pool_unittests.cc libhttp_unittests_SOURCES += date_time_unittests.cc +libhttp_unittests_SOURCES += http_header_unittests.cc libhttp_unittests_SOURCES += listener_unittests.cc libhttp_unittests_SOURCES += post_request_unittests.cc libhttp_unittests_SOURCES += post_request_json_unittests.cc diff --git a/src/lib/http/tests/http_header_unittests.cc b/src/lib/http/tests/http_header_unittests.cc new file mode 100644 index 0000000000..df9d5bbfbb --- /dev/null +++ b/src/lib/http/tests/http_header_unittests.cc @@ -0,0 +1,54 @@ +// Copyright (C) 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 +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include +#include +#include +#include + +using namespace isc; +using namespace isc::http; + +namespace { + +// Test that HTTP header can be created. +TEST(HttpHeader, create) { + HttpHeader hdr("Content-Type", "application/json"); + EXPECT_EQ("Content-Type", hdr.getName()); + EXPECT_EQ("application/json", hdr.getValue()); +} + +// Test that the numeric value can be retrieved from a header and that +// an exception is thrown if the header value is not a valid number. +TEST(HttpHeader, getUint64Value) { + HttpHeader hdr64("Content-Length", "64"); + EXPECT_EQ(64, hdr64.getUint64Value()); + + HttpHeader hdr_foo("Content-Length", "foo"); + EXPECT_THROW(hdr_foo.getUint64Value(), isc::BadValue); +} + +// Test that header name can be retrieved in lower case. +TEST(HttpHeader, getLowerCaseName) { + HttpHeader hdr("ConnectioN", "Keep-Alive"); + EXPECT_EQ("connection", hdr.getLowerCaseName()); +} + +// Test that header value can be retrieved in lower case. +TEST(HttpHeader, getLowerCaseValue) { + HttpHeader hdr("Connection", "Keep-Alive"); + EXPECT_EQ("keep-alive", hdr.getLowerCaseValue()); +} + +// Test that header value comparison is case insensitive. +TEST(HttpHeader, equalsCaseInsensitive) { + HttpHeader hdr("Connection", "KeEp-ALIve"); + EXPECT_TRUE(hdr.isValueEqual("keep-alive")); + EXPECT_TRUE(hdr.isValueEqual("KEEP-ALIVE")); + EXPECT_TRUE(hdr.isValueEqual("kEeP-AlIvE")); +} + +} // end of anonymous namespace diff --git a/src/lib/http/tests/request_parser_unittests.cc b/src/lib/http/tests/request_parser_unittests.cc index 8860ff8760..049a1bd676 100644 --- a/src/lib/http/tests/request_parser_unittests.cc +++ b/src/lib/http/tests/request_parser_unittests.cc @@ -224,6 +224,22 @@ TEST_F(HttpRequestParserTest, getLowerCase) { EXPECT_EQ(1, request_.getHttpVersion().minor_); } +// This test verifies that headers are case insensitive. +TEST_F(HttpRequestParserTest, headersCaseInsensitive) { + std::string http_req = "get /foo/bar HTTP/1.1\r\n" + "Content-type: text/html\r\n" + "connection: keep-Alive\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_.getHeader("Content-Type")->getValue()); + EXPECT_EQ("keep-alive", request_.getHeader("Connection")->getLowerCaseValue()); + EXPECT_EQ(1, request_.getHttpVersion().major_); + EXPECT_EQ(1, request_.getHttpVersion().minor_); +} + // This test verifies that other value of the HTTP version can be // specified in the request. TEST_F(HttpRequestParserTest, http20) {