]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[5448] HTTP headers are case insensitive.
authorMarcin Siodelski <marcin@isc.org>
Mon, 11 Dec 2017 20:55:46 +0000 (21:55 +0100)
committerMarcin Siodelski <marcin@isc.org>
Mon, 11 Dec 2017 20:55:46 +0000 (21:55 +0100)
src/lib/http/Makefile.am
src/lib/http/http_header.cc [new file with mode: 0644]
src/lib/http/http_header.h [new file with mode: 0644]
src/lib/http/request.cc
src/lib/http/request.h
src/lib/http/tests/Makefile.am
src/lib/http/tests/http_header_unittests.cc [new file with mode: 0644]
src/lib/http/tests/request_parser_unittests.cc

index 2b0eed5752672bb284148cb881988c0f8237518e..599711500c6ef1307a542193eb4304388c520f93 100644 (file)
@@ -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 (file)
index 0000000..3e62cc1
--- /dev/null
@@ -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 <exceptions/exceptions.h>
+#include <http/http_header.h>
+#include <util/strutil.h>
+#include <boost/lexical_cast.hpp>
+
+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<uint64_t>(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 (file)
index 0000000..975ad35
--- /dev/null
@@ -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 <boost/shared_ptr.hpp>
+#include <string>
+
+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<HttpHeader> HttpHeaderPtr;
+
+} // end of namespace isc::http
+} // end of namespace isc
+
+#endif // HTTP_HEADER_H
index 206ab332feb2d413b104d16a1d5b2c756e44da92..b54a35ad4d70e55d705f71d8c15875aea7bb769a 100644 (file)
@@ -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<uint64_t>(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_) {
index 07cd29c7e39079bda64cf90cfffcbfbb470afa2c..cb7178efdce8749bf72142697a398e601854f5b0 100644 (file)
@@ -8,6 +8,7 @@
 #define HTTP_REQUEST_H
 
 #include <exceptions/exceptions.h>
+#include <http/http_header.h>
 #include <http/http_types.h>
 #include <http/request_context.h>
 #include <boost/shared_ptr.hpp>
@@ -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<HttpVersion> required_versions_;
 
+    /// @brief Map of HTTP headers indexed by lower case header names.
+    typedef std::map<std::string, HttpHeaderPtr> 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<std::string, std::string> 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<std::string, std::string> headers_;
+    HttpHeaderMap headers_;
 
     /// @brief Pointer to the @ref HttpRequestContext holding parsed
     /// data.
index d34853e118723b0eeb407d1a1e00842835ab3ef5..a9c637deeaa63fa352e300e7756bcc6eb45ce703 100644 (file)
@@ -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 (file)
index 0000000..df9d5bb
--- /dev/null
@@ -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 <config.h>
+#include <exceptions/exceptions.h>
+#include <http/http_header.h>
+#include <gtest/gtest.h>
+
+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
index 8860ff8760aa6663af53eee137e74d5e1d92c760..049a1bd676eeb6083297bd72f2be9b55650a0a3a 100644 (file)
@@ -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) {