From d9a4c6070c64bb1dfdf8b652a9ceebeeed5ce7d4 Mon Sep 17 00:00:00 2001 From: Niels Sascha Reedijk Date: Wed, 30 Mar 2022 07:38:57 +0100 Subject: [PATCH] NetServices: Serialize the header of a BHttpRequest to a string Change-Id: Ib1e22536a0b39dc6e9461e7993ea6784f1ea0e2f --- docs/user/netservices/HttpFields.dox | 17 ++++ docs/user/netservices/HttpRequest.dox | 48 +++++++++++ docs/user/netservices/NetServicesDefs.dox | 2 +- headers/private/netservices2/HttpFields.h | 1 + headers/private/netservices2/HttpRequest.h | 5 ++ .../network/libnetservices2/HttpFields.cpp | 10 +++ .../network/libnetservices2/HttpRequest.cpp | 83 +++++++++++++++++++ .../net/netservices2/HttpProtocolTest.cpp | 4 + 8 files changed, 169 insertions(+), 1 deletion(-) diff --git a/docs/user/netservices/HttpFields.dox b/docs/user/netservices/HttpFields.dox index 453669c296..e2fc5636db 100755 --- a/docs/user/netservices/HttpFields.dox +++ b/docs/user/netservices/HttpFields.dox @@ -476,6 +476,23 @@ namespace Network { */ +/*! + \fn void BHttpFields::AddFields(std::initializer_list< Field > fields) + \brief Add a list of fields. + + This enables you to add a list of \ref BHttpFields::Field objects. Like \ref AddField(), the + fields are added in the the original order, though if there are duplicate keys within the + \a fields list, or there are existing keys in the object, they will be grouped together in + sequence. + + \exception std::bad_alloc Error in case memory cannot be allocated. + \exception BHttpFields::InvalidInput This error indicates that some of the names or values in + the list do not adhere to the HTTP specification. + + \since Haiku R1 +*/ + + /*! \fn void BHttpFields::RemoveField(const std::string_view &name) noexcept \brief Remove all fields with the \a name. diff --git a/docs/user/netservices/HttpRequest.dox b/docs/user/netservices/HttpRequest.dox index b3bd172901..72301823e1 100644 --- a/docs/user/netservices/HttpRequest.dox +++ b/docs/user/netservices/HttpRequest.dox @@ -473,6 +473,54 @@ namespace Network { //! @} +/*! + \name Serialization +*/ + + +//! @{ + + +/*! + \fn ssize_t BHttpRequest::SerializeHeaderTo(BDataIO *target) const + \brief Serialize the HTTP Header of this request to the \a target. + + The HTTP header consists of the request line, and the fields, serialized as text according to + the HTTP specification. + + \param target The \ref BDataIO object to write the header to + + \return The total number of byte size of the header. + + \exception BSystemError In case there is an error when calling the \ref BDataIO::Write() call + of the provided \a target. Note that writing the string is \em not transactional, and that + the error can occur while a part of the header has already been written. + \exception std::bad_alloc In case it is not possible to allocate internal buffers. + + \since Haiku R1 +*/ + + +/*! + \fn BString BHttpRequest::HeaderToString() const + \brief Serialize the HTTP Header of this request to a string. + + The HTTP header consists of the request line, and the fields, serialized as text according to + the HTTP specification. + + This method can be used to debug requests. + + \return A new string that represents the HTTP request. + + \exception std::bad_alloc In case it is not possible to allocate memory for the output string. + + \since Haiku R1 +*/ + + +//! @} + + } // namespace Network } // namespace BPrivate diff --git a/docs/user/netservices/NetServicesDefs.dox b/docs/user/netservices/NetServicesDefs.dox index 0218a3a2aa..2c85193278 100644 --- a/docs/user/netservices/NetServicesDefs.dox +++ b/docs/user/netservices/NetServicesDefs.dox @@ -219,7 +219,7 @@ namespace Network { to initialize it to \c __PRETTY_FUNCTION__ by default. \param type The error type that describes what the issue was that prevented the completion of the request. - \param error Optional underlying system error. See the \ref BNetworkRequestError::ErrorType + \param errorCode Optional underlying system error. See the \ref BNetworkRequestError::ErrorType documentation on which error types expect a system error. \since Haiku R1 diff --git a/headers/private/netservices2/HttpFields.h b/headers/private/netservices2/HttpFields.h index 75da8053f3..d0cc11c2ae 100755 --- a/headers/private/netservices2/HttpFields.h +++ b/headers/private/netservices2/HttpFields.h @@ -103,6 +103,7 @@ public: // Modifiers void AddField(const std::string_view& name, const std::string_view& value); + void AddFields(std::initializer_list fields); void RemoveField(const std::string_view& name) noexcept; void RemoveField(ConstIterator it) noexcept; void MakeEmpty() noexcept; diff --git a/headers/private/netservices2/HttpRequest.h b/headers/private/netservices2/HttpRequest.h index 8fea24a677..0797242679 100644 --- a/headers/private/netservices2/HttpRequest.h +++ b/headers/private/netservices2/HttpRequest.h @@ -13,6 +13,8 @@ #include #include +class BDataIO; +class BMallocIO; class BUrl; @@ -87,6 +89,9 @@ public: void SetMethod(const BHttpMethod& method); void SetUrl(const BUrl& url); + // Serialization + ssize_t SerializeHeaderTo(BDataIO* target) const; + BString HeaderToString() const; private: friend class BHttpSession; struct Data; diff --git a/src/kits/network/libnetservices2/HttpFields.cpp b/src/kits/network/libnetservices2/HttpFields.cpp index a0bce8146c..4dae9ccf32 100755 --- a/src/kits/network/libnetservices2/HttpFields.cpp +++ b/src/kits/network/libnetservices2/HttpFields.cpp @@ -407,6 +407,16 @@ BHttpFields::AddField(const std::string_view& name, const std::string_view& valu } +void +BHttpFields::AddFields(std::initializer_list fields) +{ + for (auto& field: fields) { + if (!field.IsEmpty()) + _AddField(Field(field)); + } +} + + void BHttpFields::RemoveField(const std::string_view& name) noexcept { diff --git a/src/kits/network/libnetservices2/HttpRequest.cpp b/src/kits/network/libnetservices2/HttpRequest.cpp index feeb465b8f..3c2996bbaa 100644 --- a/src/kits/network/libnetservices2/HttpRequest.cpp +++ b/src/kits/network/libnetservices2/HttpRequest.cpp @@ -10,8 +10,11 @@ #include #include +#include #include +#include +#include #include #include @@ -219,3 +222,83 @@ BHttpRequest::SetUrl(const BUrl& url) } fData->url = url; } + + +[[nodiscard]] static inline ssize_t +_write_to_dataio(BDataIO* target, const std::string_view& data) +{ + if (auto status = target->WriteExactly(data.data(), data.size()); status != B_OK) + throw BSystemError("BDataIO::WriteExactly()", status); + return data.size(); +} + + +[[nodiscard]] static inline ssize_t +_write_to_dataio(BDataIO* target, const BString& string) +{ + auto length = string.Length(); + if (auto status = target->WriteExactly(string.String(), length); status != B_OK) + throw BSystemError("BDataIO::WriteExactly()", status); + return length; +} + + +ssize_t +BHttpRequest::SerializeHeaderTo(BDataIO* target) const +{ + auto bytesWritten = _write_to_dataio(target, fData->method.Method()); + bytesWritten += _write_to_dataio(target, " "sv); + + // TODO: proxy + + if (fData->url.HasPath() && fData->url.Path().Length() > 0) + bytesWritten += _write_to_dataio(target, fData->url.Path()); + else + bytesWritten += _write_to_dataio(target, "/"sv); + + // TODO: switch between HTTP 1.0 and 1.1 based on configuration + bytesWritten += _write_to_dataio(target, " HTTP/1.1\r\n"sv); + + BHttpFields outputFields; + if (true /* http == 1.1 */) { + BString host = fData->url.Host(); + int defaultPort = fData->url.Protocol() == "http" ? 80 : 443; + if (fData->url.HasPort() && fData->url.Port() != defaultPort) + host << ':' << fData->url.Port(); + + outputFields.AddFields({ + {"Host"sv, std::string_view(host.String())}, + {"Accept"sv, "*"sv}, + {"Accept-Encoding"sv, "gzip"sv}, + // Allows the server to compress data using the "gzip" format. + // "deflate" is not supported, because there are two interpretations + // of what it means (the RFC and Microsoft products), and we don't + // want to handle this. Very few websites support only deflate, + // and most of them will send gzip, or at worst, uncompressed data. + {"Connection"sv, "close"sv} + // Let the remote server close the connection after response since + // we don't handle multiple request on a single connection + }); + } + + for (const auto& field: outputFields) { + std::string_view name = field.Name(); + bytesWritten += _write_to_dataio(target, name); + bytesWritten += _write_to_dataio(target, ": "sv); + bytesWritten += _write_to_dataio(target, field.Value()); + bytesWritten += _write_to_dataio(target, "\r\n"sv); + } + + bytesWritten += _write_to_dataio(target, "\r\n"sv); + return bytesWritten; +} + + +BString +BHttpRequest::HeaderToString() const +{ + BMallocIO buffer; + auto size = SerializeHeaderTo(&buffer); + + return BString(static_cast(buffer.Buffer()), size); +} diff --git a/src/tests/kits/net/netservices2/HttpProtocolTest.cpp b/src/tests/kits/net/netservices2/HttpProtocolTest.cpp index ab1e21407b..451509090c 100644 --- a/src/tests/kits/net/netservices2/HttpProtocolTest.cpp +++ b/src/tests/kits/net/netservices2/HttpProtocolTest.cpp @@ -234,6 +234,10 @@ HttpProtocolTest::HttpRequestTest() auto url = BUrl("https://www.haiku-os.org"); request.SetUrl(url); CPPUNIT_ASSERT(request.Url() == url); + + // Validate header serialization + BString header = request.HeaderToString(); + CPPUNIT_ASSERT(header.Compare("GET / HTTP/1.1\r\nHost: www.haiku-os.org\r\nAccept: *\r\nAccept-Encoding: gzip\r\nConnection: close\r\n\r\n") == 0); }