diff --git a/docs/user/netservices/HttpHeaders.dox b/docs/user/netservices/HttpHeaders.dox new file mode 100755 index 0000000000..92ba8ce891 --- /dev/null +++ b/docs/user/netservices/HttpHeaders.dox @@ -0,0 +1,332 @@ +/* + * Copyright 2021 Haiku, Inc. All rights reserved. + * Distributed under the terms of the MIT License. + * + * Authors: + * Niels Sascha Reedijk, niels.reedijk@gmail.com + * + * Corresponds to: + * headers/private/netservices2/HttpHeaders.h hrev????? + * src/kits/network/libnetservices2/HttpHeaders.cpp hrev????? + */ + + +#if __cplusplus >= 201703L + + +/*! + \file HttpHeaders.h + \ingroup netservices + \brief Provides the BHttpHeader and BHttpHeaderMap classes. +*/ + +namespace BPrivate { + +namespace Network { + + +/*! + \class BHttpHeader + \ingroup netservices + \brief Represent a HTTP header name and value pair. + + \since Haiku R1 +*/ + + +/*! + \class BHttpHeader::InvalidInput + \ingroup netservices + \brief Error that represents when a string input contains characters that are incompatible with + the HTTP specification. + + \since Haiku R1 +*/ + + +/*! + \var BString BHttpHeader::InvalidInput::input + \brief The input that contains the invalid contents. + + \since Haiku R1 +*/ + + +/*! + \fn BHttpHeader::InvalidInput::InvalidInput(const char *origin, BString input) + \brief Constructor that sets the \a origin and the invalid \a input. + + \since Haiku R1 +*/ + + +/*! + \fn virtual BString BHttpHeader::InvalidInput::DebugMessage() const override + \brief Retrieve a debug message that contains all info in this error. + + The output will be along the lines of: + \code + [Origin] Invalid format or unsupported characters in input [input] + \endcode + + \exception std::bad_alloc In the future this method may throw this + exception when the memory for the debug message cannot be allocated. + + \return A \ref BString object that contains the debug message. + + \since Haiku R1 +*/ + + +/*! + \fn virtual const char* BHttpHeader::InvalidInput::Message() const noexcept override + \brief Get a pointer to the message describing the error. + + \since Haiku R1 +*/ + + +/*! + \class BHttpHeader::EmptyHeader + \ingroup netservices + \brief Error that is raised when the HTTP header has an empty name or value when it is + serialized to and from text. + + \since Haiku R1 +*/ + + +/*! + \fn BHttpHeader::EmptyHeader::EmptyHeader(const char *origin) + \copydoc BError::BError() +*/ + + +/*! + \fn virtual const char* BHttpHeader::EmptyHeader::Message() const noexcept override + \brief Get a pointer to the message describing the error. + + \since Haiku R1 +*/ + + +/*! + \class BHttpHeader::HeaderName + \ingroup netservices + \brief Representation of a HTTP header name. + + As per the HTTP specification, header fields have a name. There are limitations to which + characters are supported. As per the specification, header field names are case insensitive. + This means that the \c content-encoding is equal to \c Content-Encoding or even + \c COnTenT-ENcOdING. + + This particular object can be empty. Headers with empty names can still be used in the + \ref BHttpHeaderMap object, though as soon as you try to serialize them to a string, the + \ref BHttpHeader::EmptyHeader exception will be raised. + + \since Haiku R1 +*/ + + +/*! + \fn bool BHttpHeader::HeaderName::IsEmpty() const noexcept + \brief Check if the header name has a value set. + + \retval true This object is empty, meaning it is set to an empty string. + \retval false This object has a valid header name. + + \since Haiku R1 +*/ + + +/*! + \fn BHttpHeader::HeaderName::operator BString() const + \brief Return a copy of the header name as a string. + + \return The header name as a \ref BString object. + + \since Haiku R1 +*/ + + +/*! + \fn BHttpHeader::HeaderName::operator std::string_view() const + \brief Return a \c std::string_view over the header name. + + \return A \c std::string_view object over the header name. + + \since Haiku R1 +*/ + + +/*! + \fn bool BHttpHeader::HeaderName::operator==(const BString &other) const noexcept + \brief Compare the header name to a string. + + \param other The \c other string to compare it to. + + The comparison is case-insensitive. So if this header name is set to \c Content-Encoding, + comparing it to \c content-encoding will return \c true. + + \retval true The current header name is equal to the \a other name. + \retval false The current header name is different from the \a other name. + + \since Haiku R1 +*/ + + +/*! + \fn bool BHttpHeader::HeaderName::operator==(const std::string_view other) const noexcept + \copydoc BHttpHeader::HeaderName::operator==(const BString &other) const noexcept +*/ + + +/*! + \fn BHttpHeader::BHttpHeader() + \brief Construct an empty HTTP Header Field. + + The name and the value of the field will both be empty. + + \since Haiku R1 +*/ + + +/*! + \fn BHttpHeader::BHttpHeader(BHttpHeader &&other) noexcept + \brief Move constructor. + + The name and value from the \a other header object will be moved to this object. The \a other + object will be empty, meaning it no longer has a name or value. + + \since Haiku R1 +*/ + + +/*! + \fn BHttpHeader::BHttpHeader(const BHttpHeader &other) + \brief Copy constructor. + + \since Haiku R1 +*/ + + +/*! + \fn BHttpHeader::BHttpHeader(std::string_view name, std::string_view value) + \brief Constructor to create a header from a \a name and a \a value. + + The parameters are checked whether they only contain characters that are allowed by the HTTP + specification. + + \param name The name of the header field. + \param value The value of the header field. + + \exception BHttpHeader::InvalidInput This error indicates that the \a name or the \a value + contains invalid characters. + + \since Haiku R1 +*/ + + +/*! + \fn BHttpHeader::~BHttpHeader() noexcept + \brief Destructor. + + \since Haiku R1 +*/ + + +/*! + \fn bool BHttpHeader::IsEmpty() noexcept + \brief Check if the name or the value are empty. + + A header is considered empty when it does not have a name or a value, or neither of them. An + empty header cannot be serialized to a string. + + \retval true The name or value are empty. + \retval false The name and value contain valid data. + + \since Haiku R1 +*/ + + +/*! + \fn const HeaderName& BHttpHeader::Name() noexcept + \brief Get the header name. + + \return A reference to the header name object. + + \since Haiku R1 +*/ + + +/*! + \fn std::string_view BHttpHeader::Value() noexcept + \brief Get the header value. + + \return A \c std::string_view to the header value. + + \since Haiku R1 +*/ + + +/*! + \fn BHttpHeader& BHttpHeader::operator=(BHttpHeader &&other) noexcept + \brief Move assignment operator. + + Moves the name and value from the \a other header to this object. The \a other object will be + empty. + + \since Haiku R1 +*/ + + +/*! + \fn BHttpHeader& BHttpHeader::operator=(const BHttpHeader &other) + \brief Copy assignment operator. + + Make a new header object with a copy of the name and value of the \a other header. + + \since Haiku R1 +*/ + + +/*! + \fn void BHttpHeader::SetName(std::string_view name) + \brief Set the name of the header to a \a name. + + \param name A header name with characters supported by the HTTP specification. + + \exception BHttpHeader::InvalidInput This error indicates that the \a name contains invalid + characters. + + \since Haiku R1 +*/ + + +/*! + \fn void BHttpHeader::SetValue(std::string_view value) + \brief Set the value of the header to a \a value. + + \param value A header value with characters supported by the HTTP specification. + + \exception BHttpHeader::InvalidInput This error indicates that the \a value contains invalid + characters. + + \since Haiku R1 +*/ + + +/*! + \class BHttpHeaderMap + \ingroup netservices + \brief Represent set of HTTP headers. + + \since Haiku R1 +*/ + + +} // namespace Network + +} // namespace BPrivate + +#endif diff --git a/headers/private/netservices2/HttpHeaders.h b/headers/private/netservices2/HttpHeaders.h new file mode 100755 index 0000000000..670e73d1c1 --- /dev/null +++ b/headers/private/netservices2/HttpHeaders.h @@ -0,0 +1,94 @@ +/* + * Copyright 2021 Haiku Inc. All rights reserved. + * Distributed under the terms of the MIT License. + */ + +#ifndef _B_HTTP_HEADERS_H_ +#define _B_HTTP_HEADERS_H_ + +#include + +#include +#include + + +namespace BPrivate { + +namespace Network { + + +class BHttpHeader { +public: + // Exceptions + class InvalidInput : public BError { + public: + InvalidInput(const char* origin, BString input); + + virtual const char* Message() const noexcept override; + virtual BString DebugMessage() const override; + + BString input; + }; + + class EmptyHeader : public BError { + public: + EmptyHeader(const char* origin); + + virtual const char* Message() const noexcept override; + }; + + // Wrapper Types + class HeaderName { + public: + bool IsEmpty() const noexcept { return fName.IsEmpty(); } + + // Comparison + bool operator==(const BString& other) const noexcept; + bool operator==(const std::string_view other) const noexcept; + + // Conversion + operator BString() const; + operator std::string_view() const; + private: + friend class BHttpHeader; + HeaderName(std::string_view name); + BString fName; + }; + + // Constructors & Destructor + BHttpHeader(); + BHttpHeader(std::string_view name, std::string_view value); + BHttpHeader(const BHttpHeader& other); + BHttpHeader(BHttpHeader&& other) noexcept; + ~BHttpHeader() noexcept; + + // Assignment operators + BHttpHeader& operator=(const BHttpHeader& other); + BHttpHeader& operator=(BHttpHeader&& other) noexcept; + + // Header data modification + void SetName(std::string_view name); + void SetValue(std::string_view value); + + // Access Data + const HeaderName& Name() noexcept; + std::string_view Value() noexcept; + bool IsEmpty() noexcept; + +private: + HeaderName fName; + BString fValue; +}; + + +// Placeholder for a HashMap that represents HTTP Headers +class BHttpHeaderMap { + +}; + + +} // namespace Network + +} // namespace BPrivate + +#endif // _B_HTTP_HEADERS_H_ diff --git a/src/kits/network/libnetservices2/HttpHeaders.cpp b/src/kits/network/libnetservices2/HttpHeaders.cpp new file mode 100755 index 0000000000..b0853f148d --- /dev/null +++ b/src/kits/network/libnetservices2/HttpHeaders.cpp @@ -0,0 +1,225 @@ +/* + * Copyright 2021 Haiku Inc. All rights reserved. + * Distributed under the terms of the MIT License. + * + * Authors: + * Niels Sascha Reedijk, niels.reedijk@gmail.com + */ + +#include + +#include +#include + +using namespace BPrivate::Network; + + +// #pragma mark -- utilities + + +/*! + \brief Validate whether the string conforms to a HTTP token value + + RFC 7230 section 3.2.6 determines that valid tokens for the header name are: + !#$%&'*+=.^_`|~, any digits or alpha. + + \returns \c true if the string is valid, or \c false if it is not. +*/ +static inline bool +validate_token_string(std::string_view string) +{ + for (auto it = string.cbegin(); it < string.cend(); it++) { + if (*it <= 31 || *it == 127 || *it == '(' || *it == ')' || *it == '<' || *it == '>' + || *it == '@' || *it == ',' || *it == ';' || *it == '\\' || *it == '"' + || *it == '/' || *it == '[' || *it == ']' || *it == '?' || *it == '=' + || *it == '{' || *it == '}' || *it == ' ') + return false; + } + return true; +} + + +/*! + \brief Validate whether the string is a valid HTTP header value + + RFC 7230 section 3.2.6 determines that valid tokens for the header are: + HTAB ('\t'), SP (32), all visible ASCII characters (33-126), and all characters that + not control characters (in the case of a char, any value < 0) + + \note When printing out the HTTP header, sometimes the string needs to be quoted and some + characters need to be escaped. This function is not checking for whether the string can + be transmitted as is. + + \returns \c true if the string is valid, or \c false if it is not. +*/ +static inline bool +validate_value_string(std::string_view string) +{ + for (auto it = string.cbegin(); it < string.cend(); it++) { + if ((*it >= 0 && *it < 32) || *it == 127 || *it == '\t') + return false; + } + return true; +} + + +// #pragma mark -- BHttpHeader::InvalidHeader + + +BHttpHeader::InvalidInput::InvalidInput(const char* origin, BString input) + : + BError(origin), + input(std::move(input)) +{ + +} + + +const char* +BHttpHeader::InvalidInput::Message() const noexcept +{ + return "Invalid format or unsupported characters in input"; +} + + +BString +BHttpHeader::InvalidInput::DebugMessage() const +{ + BString output = BError::DebugMessage(); + output << "\t " << input << "\n"; + return output; +} + + +// #pragma mark -- BHttpHeader::EmptyHeader + + +BHttpHeader::EmptyHeader::EmptyHeader(const char* origin) + : + BError(origin) +{ + +} + + +const char* +BHttpHeader::EmptyHeader::Message() const noexcept +{ + return "Cannot convert this object into a HTTP header string: the name or value is empty"; +} + + +// #pragma mark -- BHttpHeader::HeaderName + + +BHttpHeader::HeaderName::HeaderName(std::string_view name) +{ + if (name.empty()) { + // ignore an empty name + } else if (!validate_token_string(name)) { + throw InvalidInput(__PRETTY_FUNCTION__, BString(name.data(), name.size())); + } else { + fName.SetTo(name.data(), name.length()); + fName.CapitalizeEachWord(); + } +} + + +bool +BHttpHeader::HeaderName::operator==(const BString& other) const noexcept +{ + return fName.ICompare(other) == 0; +} + + +bool +BHttpHeader::HeaderName::operator==(const std::string_view other) const noexcept +{ + return fName.ICompare(other.data(), other.size()) == 0; +} + + +BHttpHeader::HeaderName::operator BString() const +{ + return fName; +} + + +BHttpHeader::HeaderName::operator std::string_view() const +{ + return std::string_view(fName.String()); +} + + +// #pragma mark -- BHttpHeader + + +BHttpHeader::BHttpHeader() + : fName(std::string_view()) +{ + +} + + +BHttpHeader::BHttpHeader(std::string_view name, std::string_view value) + : fName(name) +{ + SetValue(value); +} + + +BHttpHeader::BHttpHeader(const BHttpHeader& other) = default; + + +BHttpHeader::BHttpHeader(BHttpHeader&& other) noexcept = default; + + +BHttpHeader::~BHttpHeader() noexcept +{ + +} + + +BHttpHeader& +BHttpHeader::operator=(const BHttpHeader& other) = default; + + +BHttpHeader& +BHttpHeader::operator=(BHttpHeader&& other) noexcept = default; + + +void +BHttpHeader::SetName(std::string_view name) +{ + fName = BHttpHeader::HeaderName(name); +} + + +void +BHttpHeader::SetValue(std::string_view value) +{ + if (!validate_value_string(value)) + throw InvalidInput(__PRETTY_FUNCTION__, BString(value.data(), value.length())); + fValue.SetTo(value.data(), value.length()); +} + + +const BHttpHeader::HeaderName& +BHttpHeader::Name() noexcept +{ + return fName; +} + + +std::string_view +BHttpHeader::Value() noexcept +{ + return std::string_view(fValue.String()); +} + + +bool +BHttpHeader::IsEmpty() noexcept +{ + return fName.IsEmpty() || fValue.IsEmpty(); +} diff --git a/src/kits/network/libnetservices2/Jamfile b/src/kits/network/libnetservices2/Jamfile index 8682aea358..39a90f92ad 100644 --- a/src/kits/network/libnetservices2/Jamfile +++ b/src/kits/network/libnetservices2/Jamfile @@ -12,8 +12,11 @@ for architectureObject in [ MultiArchSubDirSetup ] { continue ; } + SubDirC++Flags -std=gnu++17 ; + StaticLibrary [ MultiArchDefaultGristFiles libnetservices2.a ] : ErrorsExt.cpp + HttpHeaders.cpp HttpSession.cpp NetServicesMisc.cpp ; diff --git a/src/tests/kits/net/netservices2/HttpProtocolTest.cpp b/src/tests/kits/net/netservices2/HttpProtocolTest.cpp new file mode 100644 index 0000000000..784dd036bf --- /dev/null +++ b/src/tests/kits/net/netservices2/HttpProtocolTest.cpp @@ -0,0 +1,91 @@ +/* + * Copyright 2021 Haiku Inc. All rights reserved. + * Distributed under the terms of the MIT License. + * + * Authors: + * Niels Sascha Reedijk, niels.reedijk@gmail.com + */ + +#include "HttpProtocolTest.h" + +#include +#include +#include + +#include + +using BPrivate::Network::BHttpHeader; +using BPrivate::Network::BHttpSession; + + +HttpProtocolTest::HttpProtocolTest() +{ + +} + + +void +HttpProtocolTest::HttpHeaderTest() +{ + using namespace std::literals; + + // Header field name validation (ignore value validation) + { + try { + auto validFieldName = "Content-Encoding"sv; + auto header = BHttpHeader{validFieldName, ""sv}; + } catch (...) { + CPPUNIT_FAIL("Unexpected exception when passing valid field name"); + } + try { + auto invalidFieldName = "Cóntênt_Éncõdìng"; + auto header = BHttpHeader{invalidFieldName, ""sv}; + CPPUNIT_FAIL("Creating a header with an invalid name did not raise an exception"); + } catch (const BHttpHeader::InvalidInput& e) { + // success + } catch (...) { + CPPUNIT_FAIL("Unexpected exception when creating a header with an invalid name"); + } + } + // Header field value validation (ignore name validation) + { + try { + auto validFieldValue = "VálìdF|êldValue"sv; + auto header = BHttpHeader{""sv, validFieldValue}; + } catch (...) { + CPPUNIT_FAIL("Unexpected exception when passing valid field value"); + } + try { + auto invalidFieldValue = "Invalid\tField\0Value"; + auto header = BHttpHeader{""sv, invalidFieldValue}; + CPPUNIT_FAIL("Creating a header with an invalid value did not raise an exception"); + } catch (const BHttpHeader::InvalidInput& e) { + // success + } catch (...) { + CPPUNIT_FAIL("Unexpected exception when creating a header with an invalid value"); + } + } + + // Header field name case insensitive comparison + { + BHttpHeader header = BHttpHeader{"content-type"sv, ""sv}; + CPPUNIT_ASSERT(header.Name() == "content-type"sv); + CPPUNIT_ASSERT(header.Name() == "Content-Type"sv); + CPPUNIT_ASSERT(header.Name() == "cOnTeNt-TyPe"sv); + CPPUNIT_ASSERT(header.Name() != "content_type"sv); + CPPUNIT_ASSERT(header.Name() == BString{"Content-Type"}); + } +} + + +/* static */ void +HttpProtocolTest::AddTests(BTestSuite& parent) +{ + CppUnit::TestSuite& suite = *new CppUnit::TestSuite("HttpProtocolTest"); + + suite.addTest(new CppUnit::TestCaller( + "HttpProtocolTest::HttpHeaderTest", &HttpProtocolTest::HttpHeaderTest)); + + // leak for now + parent.addTest("HttpProtocolTest", &suite); +} diff --git a/src/tests/kits/net/netservices2/HttpTest.h b/src/tests/kits/net/netservices2/HttpProtocolTest.h similarity index 78% rename from src/tests/kits/net/netservices2/HttpTest.h rename to src/tests/kits/net/netservices2/HttpProtocolTest.h index d9c1afe3c1..47ab7bbbbc 100644 --- a/src/tests/kits/net/netservices2/HttpTest.h +++ b/src/tests/kits/net/netservices2/HttpProtocolTest.h @@ -13,9 +13,11 @@ using BPrivate::Network::BHttpSession; -class HttpTest: public BTestCase { +class HttpProtocolTest: public BTestCase { public: - HttpTest(BHttpSession& session); + HttpProtocolTest(); + + void HttpHeaderTest(); static void AddTests(BTestSuite& suite); diff --git a/src/tests/kits/net/netservices2/HttpTest.cpp b/src/tests/kits/net/netservices2/HttpTest.cpp deleted file mode 100644 index cad31be102..0000000000 --- a/src/tests/kits/net/netservices2/HttpTest.cpp +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2021 Haiku Inc. All rights reserved. - * Distributed under the terms of the MIT License. - * - * Authors: - * Niels Sascha Reedijk, niels.reedijk@gmail.com - */ - -#include "HttpTest.h" - -#include - -using BPrivate::Network::BHttpSession; - - -HttpTest::HttpTest(BHttpSession& session) - :fSession(session) -{ - -} - - -/* static */ void -HttpTest::AddTests(BTestSuite& parent) -{ - CppUnit::TestSuite& suite = *new CppUnit::TestSuite("HttpTest"); - - BHttpSession session; - - HttpTest* httpTest = new HttpTest(session); - // leak for now - parent.addTest("HttpTest", &suite); -} diff --git a/src/tests/kits/net/netservices2/Jamfile b/src/tests/kits/net/netservices2/Jamfile index e38d843ead..173be3f2d4 100644 --- a/src/tests/kits/net/netservices2/Jamfile +++ b/src/tests/kits/net/netservices2/Jamfile @@ -4,10 +4,12 @@ if $(TARGET_PACKAGING_ARCH) != x86_gcc2 { # do not target the legacy platform UsePrivateHeaders netservices2 ; + SubDirC++Flags -std=gnu++17 ; + UnitTestLib netservicekit2test.so : ServicesKitTestAddon.cpp - HttpTest.cpp + HttpProtocolTest.cpp : be libnetservices2.a $(TARGET_NETWORK_LIBS) $(HAIKU_NETAPI_LIB) [ TargetLibstdc++ ] diff --git a/src/tests/kits/net/netservices2/ServicesKitTestAddon.cpp b/src/tests/kits/net/netservices2/ServicesKitTestAddon.cpp index e261cc8091..c146292009 100644 --- a/src/tests/kits/net/netservices2/ServicesKitTestAddon.cpp +++ b/src/tests/kits/net/netservices2/ServicesKitTestAddon.cpp @@ -7,7 +7,7 @@ #include #include -#include "HttpTest.h" +#include "HttpProtocolTest.h" BTestSuite* @@ -15,7 +15,7 @@ getTestSuite() { BTestSuite* suite = new BTestSuite("NetServices2Kit"); - HttpTest::AddTests(*suite); + HttpProtocolTest::AddTests(*suite); return suite; }