NetServices: Introduce BHttpFields to query and manipulate fields in HTTP messages
HTTP messages (requests and responses) have a header section that can contain HTTP headers. These headers consist of name, value pairs. This class can be used to query the headers on a response, and build a list of headers for a request. The internal implementation is designed around two different methods of storing the underlying data. For HTTP requests, the name, value pairs are stored as owned BString objects. For responses, the assumption is that there is a byte buffer that contains the data and that has the same lifetime as the BHttpFields object. The name, value pairs will then be stored as std::string_view to the underlying buffer. Still to do is: - The method to convert a BHttpFields list into a string buffer to transmit. - The method to parse a string buffer and turn it into a BHttpFields object. Change-Id: I4819db100aa1671aa7403675216a4c85fd221da7
This commit is contained in:
Executable
+585
@@ -0,0 +1,585 @@
|
||||
/*
|
||||
* Copyright 2022 Haiku, Inc. All rights reserved.
|
||||
* Distributed under the terms of the MIT License.
|
||||
*
|
||||
* Authors:
|
||||
* Niels Sascha Reedijk, [email protected]
|
||||
*
|
||||
* Corresponds to:
|
||||
* headers/private/netservices2/HttpFields.h hrev?????
|
||||
* src/kits/network/libnetservices2/HttpFields.cpp hrev?????
|
||||
*/
|
||||
|
||||
|
||||
#if __cplusplus >= 201703L
|
||||
|
||||
|
||||
/*!
|
||||
\file HttpFields.h
|
||||
\ingroup netservices
|
||||
\brief Provides the BHttpFields class.
|
||||
*/
|
||||
|
||||
namespace BPrivate {
|
||||
|
||||
namespace Network {
|
||||
|
||||
|
||||
/*!
|
||||
\class BHttpFields
|
||||
\ingroup netservices
|
||||
\brief Represents the field section of a HTTP header.
|
||||
|
||||
The HTTP protocol (RFC 7230) specifies that each HTTP request and response has a header. Part
|
||||
of that header is a list of fields, which are name and value pairs. The high level protocol
|
||||
defines what valid field names and field values look like. When adding or modifying field data,
|
||||
the members of this class enforce those constraints.
|
||||
|
||||
When you are processing a HTTP response, this object gives you the methods to query the headers
|
||||
in that response. When you are creating a HTTP request, this object gives you methods to add
|
||||
and modify header fields on your request. When retrieving data from the header fields, this
|
||||
data is often returned as an \c std::string_view. Please note that this object will only point
|
||||
to valid data for the lifetime of this object, which in case of a HTTP response, will be
|
||||
bound to the lifetime of the object that contains the HTTP response.
|
||||
|
||||
When adding headers, the fields are stored in the order in which they were added. However,
|
||||
when you add additional values with an existing name, the new field will be added below the
|
||||
existing field.
|
||||
|
||||
The HTTP protocol does not prohibit multiple fields with the same name, but it does note that
|
||||
semantically this is only allowed for a limited set of explicitly named headers, like the
|
||||
'Set-Cookie' field (see RFC 7230 section 3.2.2). Because most header fields will only exist
|
||||
once, the interface of this class is optimized for each header field existing only once. The
|
||||
onus is on the user to take additional steps when dealing with header fields that they know
|
||||
can occur more than once.
|
||||
|
||||
\since Haiku R1
|
||||
*/
|
||||
|
||||
|
||||
/*!
|
||||
\class BHttpFields::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 BHttpFields::InvalidInput::input
|
||||
\brief The input that contains the invalid contents.
|
||||
|
||||
\since Haiku R1
|
||||
*/
|
||||
|
||||
|
||||
/*!
|
||||
\fn BHttpFields::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 BHttpFields::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* BHttpFields::InvalidInput::Message() const noexcept override
|
||||
\brief Get a pointer to the message describing the error.
|
||||
|
||||
\since Haiku R1
|
||||
*/
|
||||
|
||||
|
||||
/*!
|
||||
\class BHttpFields::Field
|
||||
\ingroup netservices
|
||||
\brief Represents a HTTP header field.
|
||||
|
||||
This type represents a combination of a field name and a field value. In order to be used in
|
||||
a HTTP header, each object must contain data that is in compliance with the HTTP specification
|
||||
(RFC 7230).
|
||||
|
||||
Some official HTTP specifications give additional guidelines for how to interpret specific
|
||||
fields. This class, however, does not supply any additional parsing for those specializations.
|
||||
|
||||
Manipulation of the contents of a HTTP field will in most cases be done through the interface
|
||||
of the \ref BHttpFields object that owns this field.
|
||||
|
||||
This type has a special 'empty' state. This means that they do not have a key and value. Empty
|
||||
objects only come into existence when explicitly instantiated with the constructor with no
|
||||
arguments, or after the contents has been moved to another object. Empty objects cannot be
|
||||
added to \ref BHttpFields objets. You do not have to check for empty fields when working with
|
||||
fields coming from \ref BHttpFields objects.
|
||||
|
||||
\since Haiku R1
|
||||
*/
|
||||
|
||||
|
||||
/*!
|
||||
\fn BHttpFields::Field::Field() noexcept
|
||||
\brief Construct empty field.
|
||||
|
||||
This constructs an empty field. Because empty fields cannot be used in combination with a
|
||||
\ref BHttpFields object, it is unlikely that you will construct these empty fields yourself.
|
||||
|
||||
\since Haiku R1
|
||||
*/
|
||||
|
||||
|
||||
/*!
|
||||
\fn BHttpFields::Field::Field(const std::string_view& name, const std::string_view& value)
|
||||
\brief Construct a field with 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 std::bad_alloc Error in case memory cannot be allocated.
|
||||
\exception BHttpFields::InvalidInput This error indicates that the \a name or the \a value
|
||||
is empty or contains invalid characters.
|
||||
|
||||
\since Haiku R1
|
||||
*/
|
||||
|
||||
|
||||
/*!
|
||||
\fn BHttpFields::Field::Field(const Field &other)
|
||||
\brief Copy constructor.
|
||||
|
||||
\param other The other field to copy data from.
|
||||
|
||||
\exception std::bad_alloc Error in case memory cannot be allocated.
|
||||
|
||||
\since Haiku R1
|
||||
*/
|
||||
|
||||
|
||||
/*!
|
||||
\fn BHttpFields::Field::Field(Field&& other) noexcept
|
||||
\brief Move constructor.
|
||||
|
||||
After moving, the \a other field object will be an empty field.
|
||||
|
||||
\since Haiku R1
|
||||
*/
|
||||
|
||||
|
||||
/*!
|
||||
\fn Field& BHttpFields::Field::operator=(const Field &other)
|
||||
\brief Copy assignment.
|
||||
|
||||
\param other The other field to copy data from.
|
||||
|
||||
\exception std::bad_alloc Error in case memory cannot be allocated.
|
||||
|
||||
\since Haiku R1
|
||||
*/
|
||||
|
||||
|
||||
/*!
|
||||
\fn Field& BHttpFields::Field::operator=(Field &&other) noexcept
|
||||
\brief Move assignment.
|
||||
|
||||
After moving, the \a other field object will be an empty field.
|
||||
|
||||
\since Haiku R1
|
||||
*/
|
||||
|
||||
|
||||
/*!
|
||||
\fn const FieldName& BHttpFields::Field::Name() const noexcept
|
||||
\brief Get a const reference to the field name.
|
||||
|
||||
\return The name of the field as a \ref BHttpFields::FieldName.
|
||||
|
||||
\since Haiku R1
|
||||
*/
|
||||
|
||||
|
||||
/*!
|
||||
\fn std::string_view BHttpFields::Field::Value() const noexcept
|
||||
\brief Get a const reference to the field value.
|
||||
|
||||
\return The contents of the field value as a \a std::string_view.
|
||||
|
||||
\since Haiku R1
|
||||
*/
|
||||
|
||||
|
||||
/*!
|
||||
\fn bool BHttpFields::Field::IsEmpty() const noexcept
|
||||
\brief Check if the field is empty or has valid data.
|
||||
|
||||
\retval true This field is empty.
|
||||
\retval false This field contains a valid name and value.
|
||||
|
||||
\since Haiku R1
|
||||
*/
|
||||
|
||||
|
||||
/*!
|
||||
\class BHttpFields::FieldName
|
||||
\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.
|
||||
|
||||
A header field name can never be empty.
|
||||
|
||||
\since Haiku R1
|
||||
*/
|
||||
|
||||
|
||||
/*!
|
||||
\fn BHttpFields::FieldName::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 BHttpFields::FieldName::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 BHttpFields::FieldName::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 BHttpFields::FieldName::operator==(const std::string_view& other) const noexcept
|
||||
\copydoc BHttpFields::FieldName::operator==(const BString &other) const noexcept
|
||||
*/
|
||||
|
||||
|
||||
/*!
|
||||
\fn bool BHttpFields::FieldName::operator==(const BHttpFields::FieldName& other) const noexcept
|
||||
\copydoc BHttpFields::FieldName::operator==(const BString &other) const noexcept
|
||||
*/
|
||||
|
||||
|
||||
/*!
|
||||
\typedef BHttpFields::ConstIterator
|
||||
\brief Define a constant iterator to iterate through the list of header fields.
|
||||
|
||||
This iterator has the same semantics as other constant iterators in the C++ standard library.
|
||||
|
||||
\since Haiku R1
|
||||
*/
|
||||
|
||||
|
||||
/*!
|
||||
\name Constructors & Destructor
|
||||
*/
|
||||
|
||||
|
||||
//! @{
|
||||
|
||||
|
||||
/*!
|
||||
\fn BHttpFields::BHttpFields()
|
||||
\brief Construct an empty list of HTTP header fields.
|
||||
|
||||
\exception std::bad_alloc Error in case memory cannot be allocated.
|
||||
|
||||
\since Haiku R1
|
||||
*/
|
||||
|
||||
|
||||
/*!
|
||||
\fn BHttpFields::BHttpFields(std::initializer_list< Field > fields)
|
||||
\brief Initialize the object with a list of fields.
|
||||
|
||||
This enables you to initialize the fields with a list of \ref BHttpFields::Field objects. Any
|
||||
empty fields will be skipped. Like \ref AddField(), this constructor keeps the fields in the
|
||||
original order, though duplicate keys will be grouped together in sequence.
|
||||
|
||||
The example below will create an object with four fields, even though five fields have been
|
||||
passed in the initializer. The last header will be reorderd to follow the other
|
||||
\c Accept-Encoding header.
|
||||
\code
|
||||
const BHttpFields defaultFields = {
|
||||
{"Host"sv, "haiku-os.org"sv},
|
||||
{"Accept-Encoding"sv, "gzip"sv}
|
||||
{"Accept"sv, "*\/*"sv},
|
||||
{},
|
||||
{"Accept-Encoding"sv, "bzip2"sv}
|
||||
};
|
||||
\endcode
|
||||
|
||||
\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 BHttpFields::BHttpFields(const BHttpFields &other)
|
||||
\brief Copy constructor.
|
||||
|
||||
\exception std::bad_alloc Error in case memory cannot be allocated.
|
||||
|
||||
\since Haiku R1
|
||||
*/
|
||||
|
||||
|
||||
/*!
|
||||
\fn BHttpFields::BHttpFields(BHttpFields &&other)
|
||||
\brief Move constructor.
|
||||
|
||||
The name and value from the \a other fields object will be moved to this object. The \a other
|
||||
object will be empty, meaning it no longer has any fields.
|
||||
|
||||
\exception std::bad_alloc Error in case memory cannot be allocated.
|
||||
|
||||
\since Haiku R1
|
||||
*/
|
||||
|
||||
|
||||
/*!
|
||||
\fn BHttpFields::~BHttpFields() noexcept
|
||||
\brief Destructor.
|
||||
|
||||
\since Haiku R1
|
||||
*/
|
||||
|
||||
|
||||
//! @}
|
||||
|
||||
|
||||
/*!
|
||||
\name Assignment operators
|
||||
*/
|
||||
|
||||
|
||||
//! @{
|
||||
|
||||
|
||||
/*!
|
||||
\fn BHttpFields& BHttpFields::operator=(BHttpFields &&other) noexcept
|
||||
\brief Move assignment operator.
|
||||
|
||||
The name and value from the \a other fields object will be moved to this object. The \a other
|
||||
object will be empty, meaning it no longer has any fields.
|
||||
|
||||
\since Haiku R1
|
||||
*/
|
||||
|
||||
|
||||
/*!
|
||||
\fn BHttpFields& BHttpFields::operator=(const BHttpFields &other)
|
||||
\brief Copy assignment operator.
|
||||
|
||||
Make a new fields object with a copy of the fields of the \a other header.
|
||||
|
||||
\exception std::bad_alloc Error in case memory cannot be allocated.
|
||||
|
||||
\since Haiku R1
|
||||
*/
|
||||
|
||||
|
||||
//! @}
|
||||
|
||||
|
||||
/*!
|
||||
\name List Access
|
||||
*/
|
||||
|
||||
|
||||
//! @{
|
||||
|
||||
/*!
|
||||
\fn const Field& BHttpFields::operator[](size_t index) const
|
||||
\brief Get the item at an \a index.
|
||||
|
||||
\param index The zero-based index of the item in the list of fields.
|
||||
|
||||
\return A const reference to the the field.
|
||||
|
||||
\exception BRuntimeError Error in case the index is out of bounds.
|
||||
|
||||
\since Haiku R1
|
||||
*/
|
||||
|
||||
|
||||
//! @}
|
||||
|
||||
|
||||
/*!
|
||||
\name Modifying the list
|
||||
*/
|
||||
|
||||
|
||||
//! @{
|
||||
|
||||
/*!
|
||||
\fn void BHttpFields::AddField(const std::string_view &name, const std::string_view &value)
|
||||
\brief Append a field with \a name and a \a value to the list of headers.
|
||||
|
||||
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 std::bad_alloc Error in case memory cannot be allocated.
|
||||
\exception BHttpFields::InvalidInput This error indicates that the \a name or the \a value
|
||||
contains invalid characters.
|
||||
|
||||
\since Haiku R1
|
||||
*/
|
||||
|
||||
|
||||
/*!
|
||||
\fn void BHttpFields::RemoveField(const std::string_view &name) noexcept
|
||||
\brief Remove all fields with the \a name.
|
||||
|
||||
If there are no fields with this name, this method does nothing. Like all operations that
|
||||
involve a field name, the name matching is case insensitive.
|
||||
|
||||
\param name The name of the field to remove.
|
||||
|
||||
\since Haiku R1
|
||||
*/
|
||||
|
||||
|
||||
/*!
|
||||
\fn void BHttpFields::RemoveField(ConstIterator it) noexcept
|
||||
\brief Remove the specific field at the location of an iterator.
|
||||
|
||||
\param it A valid iterator to the item that must be removed.
|
||||
|
||||
\since Haiku R1
|
||||
*/
|
||||
|
||||
|
||||
/*!
|
||||
\fn void BHttpFields::MakeEmpty() noexcept
|
||||
\brief Clear all fields from this header.
|
||||
|
||||
Removes all fields from the container.
|
||||
|
||||
\since Haiku R1
|
||||
*/
|
||||
|
||||
|
||||
//! @}
|
||||
|
||||
|
||||
/*!
|
||||
\name Querying
|
||||
*/
|
||||
|
||||
|
||||
//! @{
|
||||
|
||||
|
||||
/*!
|
||||
\fn ConstIterator BHttpFields::FindField(const std::string_view &name) const noexcept
|
||||
\brief Find a field with \a name.
|
||||
|
||||
In case there are more than one fields with the same name, this container will make sure that
|
||||
these are grouped together. That means that you can use the properties of the iterator to find
|
||||
the other fields.
|
||||
|
||||
\param name The name of the field to be found.
|
||||
|
||||
\return Returns a valid iterator to the first field with \a name in this container, or
|
||||
BHttpFields::end() in case the name is not found.
|
||||
|
||||
\since Haiku R1
|
||||
*/
|
||||
|
||||
|
||||
/*!
|
||||
\fn size_t BHttpFields::CountFields() const noexcept
|
||||
\brief Get the number of fields.
|
||||
|
||||
\return The number of fields in this container. If multiple fields have the same name, they
|
||||
will be counted individually.
|
||||
|
||||
\since Haiku R1
|
||||
*/
|
||||
|
||||
|
||||
//! @}
|
||||
|
||||
|
||||
/*!
|
||||
\name Range-based iteration.
|
||||
Allows the usage of this object in a for loop.
|
||||
*/
|
||||
|
||||
|
||||
//! @{
|
||||
|
||||
/*!
|
||||
\fn ConstIterator BHttpFields::begin() const noexcept
|
||||
\brief Return an iterator to the first field.
|
||||
|
||||
\since Haiku R1
|
||||
*/
|
||||
|
||||
|
||||
/*!
|
||||
\fn ConstIterator BHttpFields::end() const noexcept
|
||||
\brief Return an iterator to the end of the fields.
|
||||
|
||||
\since Haiku R1
|
||||
*/
|
||||
|
||||
|
||||
//! @}
|
||||
|
||||
|
||||
} // namespace Network
|
||||
|
||||
} // namespace BPrivate
|
||||
|
||||
#endif
|
||||
@@ -1,332 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 Haiku, Inc. All rights reserved.
|
||||
* Distributed under the terms of the MIT License.
|
||||
*
|
||||
* Authors:
|
||||
* Niels Sascha Reedijk, [email protected]
|
||||
*
|
||||
* 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
|
||||
Executable
+130
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright 2022 Haiku Inc. All rights reserved.
|
||||
* Distributed under the terms of the MIT License.
|
||||
*/
|
||||
|
||||
#ifndef _B_HTTP_FIELDS_H_
|
||||
#define _B_HTTP_FIELDS_H_
|
||||
|
||||
#include <list>
|
||||
#include <string_view>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#include <ErrorsExt.h>
|
||||
#include <String.h>
|
||||
|
||||
|
||||
namespace BPrivate {
|
||||
|
||||
namespace Network {
|
||||
|
||||
|
||||
class BHttpFields {
|
||||
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;
|
||||
};
|
||||
|
||||
// Wrapper Types
|
||||
class FieldName {
|
||||
public:
|
||||
// Comparison
|
||||
bool operator==(const BString& other) const noexcept;
|
||||
bool operator==(const std::string_view& other) const noexcept;
|
||||
bool operator==(const FieldName& other) const noexcept;
|
||||
|
||||
// Conversion
|
||||
operator BString() const;
|
||||
operator std::string_view() const;
|
||||
private:
|
||||
friend class BHttpFields;
|
||||
FieldName(const std::string_view& name);
|
||||
FieldName(BString name);
|
||||
FieldName(const FieldName& other);
|
||||
FieldName(FieldName&&) noexcept;
|
||||
FieldName& operator=(const FieldName& other);
|
||||
FieldName& operator=(FieldName&&) noexcept;
|
||||
FieldName& operator=(BString name);
|
||||
|
||||
std::variant<std::string_view, BString> fName;
|
||||
};
|
||||
|
||||
class Field {
|
||||
public:
|
||||
// Constructors
|
||||
Field() noexcept;
|
||||
Field(const std::string_view& name, const std::string_view& value);
|
||||
Field(const Field& other);
|
||||
Field(Field&&) noexcept;
|
||||
|
||||
// Assignment
|
||||
Field& operator=(const Field& other);
|
||||
Field& operator=(Field&& other) noexcept;
|
||||
|
||||
// Access Operators
|
||||
const FieldName& Name() const noexcept;
|
||||
std::string_view Value() const noexcept;
|
||||
bool IsEmpty() const noexcept;
|
||||
|
||||
private:
|
||||
friend class BHttpFields;
|
||||
|
||||
Field(const std::string_view& name, const std::string_view& value, bool borrowed);
|
||||
|
||||
FieldName fName;
|
||||
std::variant<std::string_view, BString>
|
||||
fValue;
|
||||
};
|
||||
|
||||
// Type Aliases
|
||||
using ConstIterator = std::list<Field>::const_iterator;
|
||||
|
||||
// Constructors & Destructor
|
||||
BHttpFields();
|
||||
BHttpFields(std::initializer_list<Field> fields);
|
||||
BHttpFields(const BHttpFields& other);
|
||||
BHttpFields(BHttpFields&& other);
|
||||
~BHttpFields() noexcept;
|
||||
|
||||
// Assignment operators
|
||||
BHttpFields& operator=(const BHttpFields&);
|
||||
BHttpFields& operator=(BHttpFields&&) noexcept;
|
||||
|
||||
// Access list
|
||||
const Field& operator[](size_t index) const;
|
||||
|
||||
// Modifiers
|
||||
void AddField(const std::string_view& name, const std::string_view& value);
|
||||
void RemoveField(const std::string_view& name) noexcept;
|
||||
void RemoveField(ConstIterator it) noexcept;
|
||||
void MakeEmpty() noexcept;
|
||||
|
||||
// Querying
|
||||
ConstIterator FindField(const std::string_view& name) const noexcept;
|
||||
size_t CountFields() const noexcept;
|
||||
|
||||
// Range-based iteration
|
||||
ConstIterator begin() const noexcept;
|
||||
ConstIterator end() const noexcept;
|
||||
|
||||
private:
|
||||
void _AddField(Field&& field);
|
||||
|
||||
std::list<Field> fFields;
|
||||
};
|
||||
|
||||
|
||||
} // namespace Network
|
||||
|
||||
} // namespace BPrivate
|
||||
|
||||
#endif // _B_HTTP_FIELDS_H_
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
/*
|
||||
* 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_
|
||||
+497
@@ -0,0 +1,497 @@
|
||||
/*
|
||||
* Copyright 2022 Haiku Inc. All rights reserved.
|
||||
* Distributed under the terms of the MIT License.
|
||||
*
|
||||
* Authors:
|
||||
* Niels Sascha Reedijk, [email protected]
|
||||
*/
|
||||
|
||||
#include <HttpFields.h>
|
||||
|
||||
#include <algorithm>
|
||||
#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(const 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(const 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;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
\brief Case insensitively compare two string_views.
|
||||
|
||||
Inspired by https://stackoverflow.com/a/4119881
|
||||
*/
|
||||
static inline bool
|
||||
iequals(const std::string_view& a, const std::string_view& b)
|
||||
{
|
||||
return std::equal(
|
||||
a.begin(), a.end(),
|
||||
b.begin(), b.end(),
|
||||
[](char a, char b) {
|
||||
return tolower(a) == tolower(b);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// #pragma mark -- BHttpFields::InvalidHeader
|
||||
|
||||
|
||||
BHttpFields::InvalidInput::InvalidInput(const char* origin, BString input)
|
||||
:
|
||||
BError(origin),
|
||||
input(std::move(input))
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
const char*
|
||||
BHttpFields::InvalidInput::Message() const noexcept
|
||||
{
|
||||
return "Invalid format or unsupported characters in input";
|
||||
}
|
||||
|
||||
|
||||
BString
|
||||
BHttpFields::InvalidInput::DebugMessage() const
|
||||
{
|
||||
BString output = BError::DebugMessage();
|
||||
output << "\t " << input << "\n";
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
// #pragma mark -- BHttpFields::Name
|
||||
|
||||
|
||||
BHttpFields::FieldName::FieldName(const std::string_view& name)
|
||||
: fName(name)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
BHttpFields::FieldName::FieldName(BString name)
|
||||
: fName(std::move(name))
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
/*!
|
||||
\brief Copy constructor; any borrowed field is copied into an owned field
|
||||
*/
|
||||
BHttpFields::FieldName::FieldName(const FieldName& other)
|
||||
{
|
||||
BString otherName = other;
|
||||
fName = std::move(otherName);
|
||||
}
|
||||
|
||||
|
||||
/*!
|
||||
\brief Move constructor
|
||||
|
||||
Moving leaves the other object in the empty state. It is implemented to satisfy the internal
|
||||
requirements of BHttpFields and std::list<Field>. Once an object is moved from it must no
|
||||
longer be used as an entry in a BHttpFields object.
|
||||
*/
|
||||
BHttpFields::FieldName::FieldName(FieldName&& other) noexcept
|
||||
: fName(std::move(other.fName))
|
||||
{
|
||||
other.fName = std::string_view();
|
||||
}
|
||||
|
||||
|
||||
/*!
|
||||
\brief Copy assignment; the copy is always owned
|
||||
*/
|
||||
BHttpFields::FieldName&
|
||||
BHttpFields::FieldName::operator=(const BHttpFields::FieldName& other)
|
||||
{
|
||||
BString otherName = other;
|
||||
fName = std::move(otherName);
|
||||
return *this;
|
||||
}
|
||||
|
||||
|
||||
/*!
|
||||
\brief Move assignment
|
||||
|
||||
Moving leaves the other object in the empty state. It is implemented to satisfy the internal
|
||||
requirements of BHttpFields and std::list<Field>. Once an object is moved from it must no
|
||||
longer be used as an entry in a BHttpFields object.
|
||||
*/
|
||||
BHttpFields::FieldName&
|
||||
BHttpFields::FieldName::operator=(BHttpFields::FieldName&& other) noexcept
|
||||
{
|
||||
fName = std::move(other.fName);
|
||||
other.fName = std::string_view();
|
||||
return *this;
|
||||
}
|
||||
|
||||
|
||||
/*!
|
||||
\brief Unchecked assignment of owned name
|
||||
|
||||
This should only be used interally when the name is known to be valid!
|
||||
*/
|
||||
BHttpFields::FieldName&
|
||||
BHttpFields::FieldName::operator=(BString name)
|
||||
{
|
||||
fName = std::move(name);
|
||||
return *this;
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
BHttpFields::FieldName::operator==(const BString& other) const noexcept
|
||||
{
|
||||
if (std::holds_alternative<std::string_view>(fName)) {
|
||||
return iequals(std::get<std::string_view>(fName), std::string_view(other.String()));
|
||||
} else {
|
||||
return std::get<BString>(fName).ICompare(other) == 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
BHttpFields::FieldName::operator==(const std::string_view& other) const noexcept
|
||||
{
|
||||
if (std::holds_alternative<std::string_view>(fName)) {
|
||||
return iequals(std::get<std::string_view>(fName), other);
|
||||
} else {
|
||||
return std::get<BString>(fName).ICompare(other.data(), other.size()) == 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
BHttpFields::FieldName::operator==(const BHttpFields::FieldName& other) const noexcept
|
||||
{
|
||||
if (std::holds_alternative<std::string_view>(other.fName)) {
|
||||
return *this == std::get<std::string_view>(other.fName);
|
||||
} else {
|
||||
return *this == std::get<BString>(other.fName);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
BHttpFields::FieldName::operator BString() const
|
||||
{
|
||||
if (std::holds_alternative<std::string_view>(fName)) {
|
||||
const auto& name = std::get<std::string_view>(fName);
|
||||
return BString(name.data(), name.size());
|
||||
} else {
|
||||
return std::get<BString>(fName);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BHttpFields::FieldName::operator std::string_view() const
|
||||
{
|
||||
if (std::holds_alternative<std::string_view>(fName)) {
|
||||
return std::get<std::string_view>(fName);
|
||||
} else {
|
||||
return std::string_view(std::get<BString>(fName).String());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// #pragma mark -- BHttpFields::Field
|
||||
|
||||
|
||||
BHttpFields::Field::Field() noexcept
|
||||
: fName(std::string_view()), fValue(std::string_view())
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
BHttpFields::Field::Field(const std::string_view& name, const std::string_view& value)
|
||||
: Field(name, value, false)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
/*!
|
||||
\brief Internal constructor that has the option to create an instance of a 'borrowed' item.
|
||||
*/
|
||||
BHttpFields::Field::Field(const std::string_view& name, const std::string_view& value, bool borrowed)
|
||||
: fName(name), fValue(value)
|
||||
{
|
||||
if (name.length() == 0 || !validate_token_string(name))
|
||||
throw BHttpFields::InvalidInput(__PRETTY_FUNCTION__, fName);
|
||||
if (value.length() == 0 || !validate_value_string(value))
|
||||
throw BHttpFields::InvalidInput(__PRETTY_FUNCTION__, BString(value.data(), value.length()));
|
||||
|
||||
if (!borrowed) {
|
||||
// set as owned
|
||||
fName = BHttpFields::FieldName(BString(name.data(), name.length()));
|
||||
fValue = BString(value.data(), value.length());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BHttpFields::Field::Field(const BHttpFields::Field& other)
|
||||
: fName(std::string_view()), fValue(std::string_view())
|
||||
{
|
||||
if (!other.IsEmpty()) {
|
||||
BString name = other.fName;
|
||||
fName = std::move(name);
|
||||
std::string_view otherValue = other.Value();
|
||||
fValue = BString(otherValue.data(), otherValue.length());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BHttpFields::Field::Field(BHttpFields::Field&& other) noexcept
|
||||
: fName(std::move(other.fName)), fValue(std::move(other.fValue))
|
||||
{
|
||||
other.fName.fName = std::string_view();
|
||||
other.fValue = std::string_view();
|
||||
}
|
||||
|
||||
|
||||
BHttpFields::Field&
|
||||
BHttpFields::Field::operator=(const BHttpFields::Field& other)
|
||||
{
|
||||
if (other.IsEmpty()) {
|
||||
fName = std::string_view();
|
||||
fValue = std::string_view();
|
||||
} else {
|
||||
BString name = other.fName;
|
||||
fName = std::move(name);
|
||||
std::string_view otherValue = other.Value();
|
||||
fValue = BString(otherValue.data(), otherValue.length());
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
|
||||
BHttpFields::Field&
|
||||
BHttpFields::Field::operator=(BHttpFields::Field&& other) noexcept
|
||||
{
|
||||
fName = std::move(other.fName);
|
||||
other.fName.fName = std::string_view();
|
||||
fValue = std::move(other.fValue);
|
||||
fValue = std::string_view();
|
||||
return *this;
|
||||
}
|
||||
|
||||
|
||||
const BHttpFields::FieldName&
|
||||
BHttpFields::Field::Name() const noexcept
|
||||
{
|
||||
return fName;
|
||||
}
|
||||
|
||||
|
||||
std::string_view
|
||||
BHttpFields::Field::Value() const noexcept
|
||||
{
|
||||
if (std::holds_alternative<std::string_view>(fValue)) {
|
||||
return std::get<std::string_view>(fValue);
|
||||
} else {
|
||||
return std::string_view(std::get<BString>(fValue).String());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
BHttpFields::Field::IsEmpty() const noexcept
|
||||
{
|
||||
// The object is either fully empty, or it has data, so we only have to check fValue.
|
||||
if (std::holds_alternative<std::string_view>(fValue))
|
||||
return std::get<std::string_view>(fValue).length() == 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// #pragma mark -- BHttpFields
|
||||
|
||||
|
||||
BHttpFields::BHttpFields()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
BHttpFields::BHttpFields(std::initializer_list<BHttpFields::Field> fields)
|
||||
{
|
||||
for (auto& field: fields) {
|
||||
if (!field.IsEmpty())
|
||||
_AddField(Field(field));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BHttpFields::BHttpFields(const BHttpFields& other) = default;
|
||||
|
||||
|
||||
BHttpFields::BHttpFields(BHttpFields&& other)
|
||||
: fFields(std::move(other.fFields))
|
||||
{
|
||||
// Explicitly clear the other list, as the C++ standard does not specify that the other list
|
||||
// will be empty.
|
||||
other.fFields.clear();
|
||||
}
|
||||
|
||||
|
||||
BHttpFields::~BHttpFields() noexcept
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
BHttpFields&
|
||||
BHttpFields::operator=(const BHttpFields& other) = default;
|
||||
|
||||
|
||||
BHttpFields&
|
||||
BHttpFields::operator=(BHttpFields&& other) noexcept
|
||||
{
|
||||
fFields = std::move(other.fFields);
|
||||
|
||||
// Explicitly clear the other list, as the C++ standard does not specify that the other list
|
||||
// will be empty.
|
||||
other.fFields.clear();
|
||||
return *this;
|
||||
}
|
||||
|
||||
|
||||
const BHttpFields::Field&
|
||||
BHttpFields::operator[](size_t index) const
|
||||
{
|
||||
if (index >= fFields.size())
|
||||
throw BRuntimeError(__PRETTY_FUNCTION__, "Index out of bounds");
|
||||
auto it = fFields.cbegin();
|
||||
std::advance(it, index);
|
||||
return *it;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
BHttpFields::AddField(const std::string_view& name, const std::string_view& value)
|
||||
{
|
||||
_AddField(BHttpFields::Field(name, value));
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
BHttpFields::RemoveField(const std::string_view& name) noexcept
|
||||
{
|
||||
for(auto it = FindField(name); it != end(); it = FindField(name)) {
|
||||
fFields.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
BHttpFields::RemoveField(ConstIterator it) noexcept
|
||||
{
|
||||
fFields.erase(it);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
BHttpFields::MakeEmpty() noexcept
|
||||
{
|
||||
fFields.clear();
|
||||
}
|
||||
|
||||
|
||||
BHttpFields::ConstIterator
|
||||
BHttpFields::FindField(const std::string_view& name) const noexcept
|
||||
{
|
||||
for (auto it = fFields.cbegin(); it != fFields.cend(); it++) {
|
||||
if ((*it).Name() == name)
|
||||
return it;
|
||||
}
|
||||
return fFields.cend();
|
||||
}
|
||||
|
||||
|
||||
size_t
|
||||
BHttpFields::CountFields() const noexcept
|
||||
{
|
||||
return fFields.size();
|
||||
}
|
||||
|
||||
|
||||
BHttpFields::ConstIterator
|
||||
BHttpFields::begin() const noexcept
|
||||
{
|
||||
return fFields.cbegin();
|
||||
}
|
||||
|
||||
|
||||
BHttpFields::ConstIterator
|
||||
BHttpFields::end() const noexcept
|
||||
{
|
||||
return fFields.cend();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
BHttpFields::_AddField(Field&& field)
|
||||
{
|
||||
// This could be made more efficient bay adding a set of existing keys to quickly check against
|
||||
auto rIterator = std::find_if(fFields.rbegin(), fFields.rend(), [&, field](const Field& f){
|
||||
return f.Name() == field.Name();
|
||||
});
|
||||
|
||||
if (rIterator == fFields.rend())
|
||||
fFields.push_back(field);
|
||||
else
|
||||
fFields.insert(rIterator.base(), field);
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 Haiku Inc. All rights reserved.
|
||||
* Distributed under the terms of the MIT License.
|
||||
*
|
||||
* Authors:
|
||||
* Niels Sascha Reedijk, [email protected]
|
||||
*/
|
||||
|
||||
#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();
|
||||
}
|
||||
@@ -16,7 +16,7 @@ for architectureObject in [ MultiArchSubDirSetup ] {
|
||||
|
||||
StaticLibrary [ MultiArchDefaultGristFiles libnetservices2.a ] :
|
||||
ErrorsExt.cpp
|
||||
HttpHeaders.cpp
|
||||
HttpFields.cpp
|
||||
HttpSession.cpp
|
||||
NetServicesMisc.cpp
|
||||
;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2021 Haiku Inc. All rights reserved.
|
||||
* Copyright 2022 Haiku Inc. All rights reserved.
|
||||
* Distributed under the terms of the MIT License.
|
||||
*
|
||||
* Authors:
|
||||
@@ -12,9 +12,9 @@
|
||||
#include <cppunit/TestCaller.h>
|
||||
#include <cppunit/TestSuite.h>
|
||||
|
||||
#include <HttpHeaders.h>
|
||||
#include <HttpFields.h>
|
||||
|
||||
using BPrivate::Network::BHttpHeader;
|
||||
using BPrivate::Network::BHttpFields;
|
||||
using BPrivate::Network::BHttpSession;
|
||||
|
||||
|
||||
@@ -25,23 +25,24 @@ HttpProtocolTest::HttpProtocolTest()
|
||||
|
||||
|
||||
void
|
||||
HttpProtocolTest::HttpHeaderTest()
|
||||
HttpProtocolTest::HttpFieldsTest()
|
||||
{
|
||||
using namespace std::literals;
|
||||
|
||||
// Header field name validation (ignore value validation)
|
||||
{
|
||||
auto fields = BHttpFields();
|
||||
try {
|
||||
auto validFieldName = "Content-Encoding"sv;
|
||||
auto header = BHttpHeader{validFieldName, ""sv};
|
||||
fields.AddField(validFieldName, "value"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};
|
||||
fields.AddField(invalidFieldName, "value"sv);
|
||||
CPPUNIT_FAIL("Creating a header with an invalid name did not raise an exception");
|
||||
} catch (const BHttpHeader::InvalidInput& e) {
|
||||
} catch (const BHttpFields::InvalidInput& e) {
|
||||
// success
|
||||
} catch (...) {
|
||||
CPPUNIT_FAIL("Unexpected exception when creating a header with an invalid name");
|
||||
@@ -49,17 +50,18 @@ HttpProtocolTest::HttpHeaderTest()
|
||||
}
|
||||
// Header field value validation (ignore name validation)
|
||||
{
|
||||
auto fields = BHttpFields();
|
||||
try {
|
||||
auto validFieldValue = "VálìdF|êldValue"sv;
|
||||
auto header = BHttpHeader{""sv, validFieldValue};
|
||||
fields.AddField("Field"sv, validFieldValue);
|
||||
} catch (...) {
|
||||
CPPUNIT_FAIL("Unexpected exception when passing valid field value");
|
||||
}
|
||||
try {
|
||||
auto invalidFieldValue = "Invalid\tField\0Value";
|
||||
auto header = BHttpHeader{""sv, invalidFieldValue};
|
||||
fields.AddField("Field"sv, invalidFieldValue);
|
||||
CPPUNIT_FAIL("Creating a header with an invalid value did not raise an exception");
|
||||
} catch (const BHttpHeader::InvalidInput& e) {
|
||||
} catch (const BHttpFields::InvalidInput& e) {
|
||||
// success
|
||||
} catch (...) {
|
||||
CPPUNIT_FAIL("Unexpected exception when creating a header with an invalid value");
|
||||
@@ -68,12 +70,100 @@ HttpProtocolTest::HttpHeaderTest()
|
||||
|
||||
// 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"});
|
||||
BHttpFields fields = BHttpFields();
|
||||
fields.AddField("content-type"sv, "value"sv);
|
||||
CPPUNIT_ASSERT(fields[0].Name() == "content-type"sv);
|
||||
CPPUNIT_ASSERT(fields[0].Name() == "Content-Type"sv);
|
||||
CPPUNIT_ASSERT(fields[0].Name() == "cOnTeNt-TyPe"sv);
|
||||
CPPUNIT_ASSERT(fields[0].Name() != "content_type"sv);
|
||||
CPPUNIT_ASSERT(fields[0].Name() == BString{"Content-Type"});
|
||||
}
|
||||
|
||||
// Set up a generic set of headers for further use
|
||||
const BHttpFields defaultFields = {
|
||||
{"Host"sv, "haiku-os.org"sv},
|
||||
{"Accept"sv, "*/*"sv},
|
||||
{"Set-Cookie"sv, "qwerty=494793ddkl; Domain=haiku-os.co.uk"sv},
|
||||
{"Set-Cookie"sv, "afbzyi=0kdnke0lyv; Domain=haiku-os.co.uk"sv},
|
||||
{}, // Empty; should be ignored by the constructor
|
||||
{"Accept-Encoding"sv, "gzip"sv}
|
||||
};
|
||||
|
||||
// Validate std::initializer_list constructor
|
||||
CPPUNIT_ASSERT_EQUAL(5, defaultFields.CountFields());
|
||||
|
||||
// Test copying and moving
|
||||
{
|
||||
BHttpFields copiedFields = defaultFields;
|
||||
CPPUNIT_ASSERT_EQUAL(copiedFields.CountFields(), defaultFields.CountFields());
|
||||
for (size_t i = 0; i < defaultFields.CountFields(); i++) {
|
||||
std::string_view copiedName = copiedFields[i].Name();
|
||||
CPPUNIT_ASSERT(defaultFields[i].Name() == copiedName);
|
||||
CPPUNIT_ASSERT_EQUAL(defaultFields[i].Value(), copiedFields[i].Value());
|
||||
}
|
||||
|
||||
BHttpFields movedFields(std::move(copiedFields));
|
||||
CPPUNIT_ASSERT_EQUAL(movedFields.CountFields(), defaultFields.CountFields());
|
||||
for (size_t i = 0; i < movedFields.CountFields(); i++) {
|
||||
std::string_view defaultName = defaultFields[i].Name();
|
||||
CPPUNIT_ASSERT(movedFields[i].Name() == defaultName);
|
||||
CPPUNIT_ASSERT_EQUAL(movedFields[i].Value(), defaultFields[i].Value());
|
||||
}
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(copiedFields.CountFields(), 0);
|
||||
}
|
||||
|
||||
// Test query and modification tools
|
||||
{
|
||||
BHttpFields fields = defaultFields;
|
||||
// test order of adding fields
|
||||
fields.AddField("Set-Cookie"sv, "vfxdrm=9lpqrsvxm; Domain=haiku-os.co.uk"sv);
|
||||
// query for Set-Cookie
|
||||
auto it = fields.FindField("Set-Cookie"sv);
|
||||
CPPUNIT_ASSERT(it != fields.end());
|
||||
CPPUNIT_ASSERT((*it).Name() == "Set-Cookie"sv);
|
||||
CPPUNIT_ASSERT_EQUAL(defaultFields[2].Value(), (*it).Value());
|
||||
it++;
|
||||
CPPUNIT_ASSERT(it != fields.end());
|
||||
CPPUNIT_ASSERT((*it).Name() == "Set-Cookie"sv);
|
||||
CPPUNIT_ASSERT_EQUAL(defaultFields[3].Value(), (*it).Value());
|
||||
it++;
|
||||
CPPUNIT_ASSERT(it != fields.end());
|
||||
CPPUNIT_ASSERT((*it).Name() == "Set-Cookie"sv);
|
||||
it++;
|
||||
CPPUNIT_ASSERT(it != fields.end());
|
||||
CPPUNIT_ASSERT_EQUAL(defaultFields[4].Value(), (*it).Value());
|
||||
// Remove the Accept-Encoding entry by iterator
|
||||
fields.RemoveField(it);
|
||||
CPPUNIT_ASSERT_EQUAL(fields.CountFields(), defaultFields.CountFields());
|
||||
// Remove the Set-Cookie entries by name
|
||||
fields.RemoveField("Set-Cookie"sv);
|
||||
CPPUNIT_ASSERT_EQUAL(fields.CountFields(), 2);
|
||||
// Test MakeEmpty
|
||||
fields.MakeEmpty();
|
||||
CPPUNIT_ASSERT_EQUAL(fields.CountFields(), 0);
|
||||
}
|
||||
|
||||
// Iterate through the fields using a constant iterator
|
||||
{
|
||||
const BHttpFields fields = {
|
||||
{"key1"sv, "value1"sv},
|
||||
{"key2"sv, "value2"sv},
|
||||
{"key3"sv, "value3"sv},
|
||||
{"key4"sv, "value4"sv}
|
||||
};
|
||||
|
||||
auto count = 0L;
|
||||
for (const auto& field: fields) {
|
||||
count++;
|
||||
auto key = BString("key");
|
||||
auto value = BString("value");
|
||||
key << count;
|
||||
value << count;
|
||||
CPPUNIT_ASSERT_EQUAL(key, field.Name());
|
||||
CPPUNIT_ASSERT_EQUAL(value, BString(field.Value().data(), field.Value().length()));
|
||||
}
|
||||
CPPUNIT_ASSERT_EQUAL(count, 4);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,8 +174,7 @@ HttpProtocolTest::AddTests(BTestSuite& parent)
|
||||
CppUnit::TestSuite& suite = *new CppUnit::TestSuite("HttpProtocolTest");
|
||||
|
||||
suite.addTest(new CppUnit::TestCaller<HttpProtocolTest>(
|
||||
"HttpProtocolTest::HttpHeaderTest", &HttpProtocolTest::HttpHeaderTest));
|
||||
"HttpProtocolTest::HttpFieldsTest", &HttpProtocolTest::HttpFieldsTest));
|
||||
|
||||
// leak for now
|
||||
parent.addTest("HttpProtocolTest", &suite);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ class HttpProtocolTest: public BTestCase {
|
||||
public:
|
||||
HttpProtocolTest();
|
||||
|
||||
void HttpHeaderTest();
|
||||
void HttpFieldsTest();
|
||||
|
||||
static void AddTests(BTestSuite& suite);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user