netservices: add BHttpHeader class and BHttpHeaderMap skeleton

Change-Id: I36a7c757a6909604d749355ecb1a9d42d05d7306
This commit is contained in:
Niels Sascha Reedijk 2021-12-15 21:55:51 +00:00
parent 892cbe10b6
commit 34522da9e3
9 changed files with 754 additions and 38 deletions

View File

@ -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

View File

@ -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 <string_view>
#include <ErrorsExt.h>
#include <String.h>
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_

View File

@ -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 <HttpHeaders.h>
#include <ctype.h>
#include <utility>
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();
}

View File

@ -12,8 +12,11 @@ for architectureObject in [ MultiArchSubDirSetup ] {
continue ; continue ;
} }
SubDirC++Flags -std=gnu++17 ;
StaticLibrary [ MultiArchDefaultGristFiles libnetservices2.a ] : StaticLibrary [ MultiArchDefaultGristFiles libnetservices2.a ] :
ErrorsExt.cpp ErrorsExt.cpp
HttpHeaders.cpp
HttpSession.cpp HttpSession.cpp
NetServicesMisc.cpp NetServicesMisc.cpp
; ;

View File

@ -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 <cppunit/TestAssert.h>
#include <cppunit/TestCaller.h>
#include <cppunit/TestSuite.h>
#include <HttpHeaders.h>
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>(
"HttpProtocolTest::HttpHeaderTest", &HttpProtocolTest::HttpHeaderTest));
// leak for now
parent.addTest("HttpProtocolTest", &suite);
}

View File

@ -13,9 +13,11 @@
using BPrivate::Network::BHttpSession; using BPrivate::Network::BHttpSession;
class HttpTest: public BTestCase { class HttpProtocolTest: public BTestCase {
public: public:
HttpTest(BHttpSession& session); HttpProtocolTest();
void HttpHeaderTest();
static void AddTests(BTestSuite& suite); static void AddTests(BTestSuite& suite);

View File

@ -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 <cppunit/TestSuite.h>
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);
}

View File

@ -4,10 +4,12 @@ if $(TARGET_PACKAGING_ARCH) != x86_gcc2 {
# do not target the legacy platform # do not target the legacy platform
UsePrivateHeaders netservices2 ; UsePrivateHeaders netservices2 ;
SubDirC++Flags -std=gnu++17 ;
UnitTestLib netservicekit2test.so : UnitTestLib netservicekit2test.so :
ServicesKitTestAddon.cpp ServicesKitTestAddon.cpp
HttpTest.cpp HttpProtocolTest.cpp
: be libnetservices2.a $(TARGET_NETWORK_LIBS) $(HAIKU_NETAPI_LIB) : be libnetservices2.a $(TARGET_NETWORK_LIBS) $(HAIKU_NETAPI_LIB)
[ TargetLibstdc++ ] [ TargetLibstdc++ ]

View File

@ -7,7 +7,7 @@
#include <TestSuite.h> #include <TestSuite.h>
#include <TestSuiteAddon.h> #include <TestSuiteAddon.h>
#include "HttpTest.h" #include "HttpProtocolTest.h"
BTestSuite* BTestSuite*
@ -15,7 +15,7 @@ getTestSuite()
{ {
BTestSuite* suite = new BTestSuite("NetServices2Kit"); BTestSuite* suite = new BTestSuite("NetServices2Kit");
HttpTest::AddTests(*suite); HttpProtocolTest::AddTests(*suite);
return suite; return suite;
} }