NetServices: Add BHttpMethod that represents a HTTP method

This class provides defaults and performs basic validation for HTTP Methods as
defined by the standard. They will be used in conjunction with a future
BHttpRequest class.

Change-Id: If69a7ec186d9d1165e8efe5ab5df50d5a089208d
This commit is contained in:
Niels Sascha Reedijk 2022-02-20 09:40:20 +00:00
parent cb67e34887
commit ec865cb87e
6 changed files with 506 additions and 0 deletions

View File

@ -0,0 +1,255 @@
/*
* Copyright 2022 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/HttpRequest.h hrev?????
* src/kits/network/libnetservices2/HttpRequest.cpp hrev?????
*/
#if __cplusplus >= 201703L
/*!
\file HttpRequest.h
\ingroup netservices
\brief Provides the classes and tools to build HTTP Requests.
\since Haiku R1
*/
namespace BPrivate {
namespace Network {
/*!
\class BHttpMethod
\ingroup netservices
\brief Represent a HTTP method.
The <a href="https://datatracker.ietf.org/doc/html/rfc7231#section-4.1">HTTP standard</a>
specifies that HTTP requests have a method. Common methods are \c GET and \c HEAD methods.
Standardized and common methods are in the form of \em verbs and are in capitalized letters
from the ASCII token set, though any valid token can be used.
It is most likely that you will not use the methods of this class directly, instead you will
use the implicit constructors while interacting with the \ref BHttpRequest class.
\code
auto url = BUrl2("https://www.haiku-os.org/");
// implicitly construct a standard get request
auto standard = BHttpRequest(url, BHttpMethod::Get);
// implicitly construct a nonstandard patch request
auto custom = BHttpRequest(url, "PATCH"sv);
\endcode
\note When you are using the standard list of verbs, there will never be an exception when
creating objects of this type. When you create a custom method, exceptions may be raised
when the system runs out of memory, or when your custom method contains invalid characters.
In almost all cases, you can probably safely assume you will not run into these exceptions,
except for cases where you use user input to create methods or you are very defensive
about memory management.
\since Haiku R1
*/
/*!
\class BHttpMethod::InvalidMethod
\ingroup netservices
\brief Error that represents when a custom method does not conform to the HTTP standard.
\since Haiku R1
*/
/*!
\var BString BHttpMethod::InvalidMethod::input
\brief The input that contains the invalid contents.
\since Haiku R1
*/
/*!
\fn BHttpMethod::InvalidMethod::InvalidMethod(const char *origin, BString input)
\brief Constructor that sets the \a origin and the invalid \a input.
\since Haiku R1
*/
/*!
\enum BHttpMethod::Verb
\ingroup netservices
\brief A list of standard HTTP methods.
\since Haiku R1
*/
/*!
\var BHttpMethod::Verb BHttpMethod::Get
\brief Represents the \c GET method.
\since Haiku R1
*/
/*!
\var BHttpMethod::Verb BHttpMethod::Head
\brief Represents the \c HEAD method.
\since Haiku R1
*/
/*!
\var BHttpMethod::Verb BHttpMethod::Post
\brief Represents the \c POST method.
\since Haiku R1
*/
/*!
\var BHttpMethod::Verb BHttpMethod::Put
\brief Represents the \c PUT method.
\since Haiku R1
*/
/*!
\var BHttpMethod::Verb BHttpMethod::Delete
\brief Represents the \c DELETE method.
\since Haiku R1
*/
/*!
\var BHttpMethod::Verb BHttpMethod::Connect
\brief Represents the \c CONNECT method.
\since Haiku R1
*/
/*!
\var BHttpMethod::Verb BHttpMethod::Options
\brief Represents the \c OPTIONS method.
\since Haiku R1
*/
/*!
\var BHttpMethod::Verb BHttpMethod::Trace
\brief Represents the \c TRACE method.
\since Haiku R1
*/
/*!
\fn BHttpMethod::BHttpMethod(BHttpMethod &&other) noexcept
\brief Move constructor.
Moves the data from the \a other to this object. The \a other object will be set to
\ref BHttpMethod::Get.
\since Haiku R1
*/
/*!
\fn BHttpMethod::BHttpMethod(const BHttpMethod &other)
\brief Copy constructor.
Copy data from an \a other object.
\exception std::bad_alloc When the \a other object contains a custom verb, this exception
will be raised if it is impossible to allocate memory.
\since Haiku R1
*/
/*!
\fn BHttpMethod::BHttpMethod(const std::string_view &method)
\brief Construct a custom method.
\param method The verb for the method.
\exception std::bad_alloc In case it is not possible to allocate memory for the custom string.
\exception BHttpMethod::InvalidMethod In case the \a method is empty or contains invalid
characters.
\since Haiku R1
*/
/*!
\fn BHttpMethod::BHttpMethod(Verb verb) noexcept
\brief Construct a standard method.
\param verb The chosen method.
\since Haiku R1
*/
/*!
\fn BHttpMethod::~BHttpMethod()
\brief Destructor.
\since Haiku R1
*/
/*!
\fn const std::string_view BHttpMethod::Method() const noexcept
\brief Get a string representation of the method.
\return A \c std::string_view that is a string representation of the standard or custom method
in this object. The lifetime of the string view is bound to the lifetime of this method.
\since Haiku R1
*/
/*!
\fn BHttpMethod& BHttpMethod::operator=(BHttpMethod &&other) noexcept
\brief Move assignment.
Moves the data from the \a other to this object. The \a other object will be set to
\ref BHttpMethod::Get.
\since Haiku R1
*/
/*!
\fn BHttpMethod& BPrivate::Network::BHttpMethod::operator=(const BHttpMethod &other)
\brief Copy assignment.
Copy data from an \a other object.
\exception std::bad_alloc When the \a other object contains a custom verb, this exception
will be raised if it is impossible to allocate memory.
\since Haiku R1
*/
} // namespace Network
} // namespace BPrivate
#endif

View File

@ -0,0 +1,69 @@
/*
* Copyright 2022 Haiku Inc. All rights reserved.
* Distributed under the terms of the MIT License.
*/
#ifndef _B_HTTP_REQUEST_H_
#define _B_HTTP_REQUEST_H_
#include <string_view>
#include <variant>
#include <ErrorsExt.h>
#include <String.h>
namespace BPrivate {
namespace Network {
class BHttpMethod {
public:
// Constants for default methods in RFC 7230 section 4.2
enum Verb {
Get,
Head,
Post,
Put,
Delete,
Connect,
Options,
Trace
};
// Error type when constructing with a custom method
class InvalidMethod : public BError {
public:
InvalidMethod(const char* origin, BString input);
virtual const char* Message() const noexcept override;
virtual BString DebugMessage() const override;
BString input;
};
// Constructors & Destructor
BHttpMethod(Verb verb) noexcept;
BHttpMethod(const std::string_view& method);
BHttpMethod(const BHttpMethod& other);
BHttpMethod(BHttpMethod&& other) noexcept;
~BHttpMethod();
// Assignment operators
BHttpMethod& operator=(const BHttpMethod& other);
BHttpMethod& operator=(BHttpMethod&& other) noexcept;
// Get the method as a string
const std::string_view Method() const noexcept;
private:
std::variant<Verb, BString> fMethod;
};
} // namespace Network
} // namespace BPrivate
#endif // B_HTTP_REQUEST

View File

@ -0,0 +1,127 @@
/*
* Copyright 2022 Haiku Inc. All rights reserved.
* Distributed under the terms of the MIT License.
*
* Authors:
* Niels Sascha Reedijk, niels.reedijk@gmail.com
*/
#include <HttpRequest.h>
#include <algorithm>
#include <ctype.h>
#include <utility>
#include "HttpPrivate.h"
using namespace std::literals;
using namespace BPrivate::Network;
// #pragma mark -- BHttpMethod::InvalidMethod
BHttpMethod::InvalidMethod::InvalidMethod(const char* origin, BString input)
:
BError(origin),
input(std::move(input))
{
}
const char*
BHttpMethod::InvalidMethod::Message() const noexcept
{
if (input.IsEmpty())
return "The HTTP method cannot be empty";
else
return "Unsupported characters in the HTTP method";
}
BString
BHttpMethod::InvalidMethod::DebugMessage() const
{
BString output = BError::DebugMessage();
if (!input.IsEmpty())
output << ":\t " << input << "\n";
return output;
}
// #pragma mark -- BHttpMethod
BHttpMethod::BHttpMethod(Verb verb) noexcept
: fMethod(verb)
{
}
BHttpMethod::BHttpMethod(const std::string_view& verb)
: fMethod(BString(verb.data(), verb.length()))
{
if (verb.size() == 0 || !validate_http_token_string(verb))
throw BHttpMethod::InvalidMethod(__PRETTY_FUNCTION__, std::move(std::get<BString>(fMethod)));
}
BHttpMethod::BHttpMethod(const BHttpMethod& other) = default;
BHttpMethod::BHttpMethod(BHttpMethod&& other) noexcept
: fMethod(std::move(other.fMethod))
{
other.fMethod = Get;
}
BHttpMethod::~BHttpMethod() = default;
BHttpMethod&
BHttpMethod::operator=(const BHttpMethod& other) = default;
BHttpMethod&
BHttpMethod::operator=(BHttpMethod&& other) noexcept
{
fMethod = std::move(other.fMethod);
other.fMethod = Get;
return *this;
}
const std::string_view
BHttpMethod::Method() const noexcept
{
if (std::holds_alternative<Verb>(fMethod)) {
switch (std::get<Verb>(fMethod)) {
case Get:
return "GET"sv;
case Head:
return "HEAD"sv;
case Post:
return "POST"sv;
case Put:
return "PUT"sv;
case Delete:
return "DELETE"sv;
case Connect:
return "CONNECT"sv;
case Options:
return "OPTIONS"sv;
case Trace:
return "TRACE"sv;
default:
// should never be reached
std::abort();
}
} else {
const auto& methodString = std::get<BString>(fMethod);
// the following constructor is not noexcept, but we know we pass in valid data
return std::string_view(methodString.String());
}
}

View File

@ -17,6 +17,7 @@ for architectureObject in [ MultiArchSubDirSetup ] {
StaticLibrary [ MultiArchDefaultGristFiles libnetservices2.a ] :
ErrorsExt.cpp
HttpFields.cpp
HttpRequest.cpp
HttpSession.cpp
NetServicesMisc.cpp
;

View File

@ -13,8 +13,10 @@
#include <cppunit/TestSuite.h>
#include <HttpFields.h>
#include <HttpRequest.h>
using BPrivate::Network::BHttpFields;
using BPrivate::Network::BHttpMethod;
using BPrivate::Network::BHttpSession;
@ -168,6 +170,55 @@ HttpProtocolTest::HttpFieldsTest()
}
void
HttpProtocolTest::HttpMethodTest()
{
using namespace std::literals;
// Default methods
{
CPPUNIT_ASSERT_EQUAL(BHttpMethod(BHttpMethod::Get).Method(), "GET"sv);
CPPUNIT_ASSERT_EQUAL(BHttpMethod(BHttpMethod::Head).Method(), "HEAD"sv);
CPPUNIT_ASSERT_EQUAL(BHttpMethod(BHttpMethod::Post).Method(), "POST"sv);
CPPUNIT_ASSERT_EQUAL(BHttpMethod(BHttpMethod::Put).Method(), "PUT"sv);
CPPUNIT_ASSERT_EQUAL(BHttpMethod(BHttpMethod::Delete).Method(), "DELETE"sv);
CPPUNIT_ASSERT_EQUAL(BHttpMethod(BHttpMethod::Connect).Method(), "CONNECT"sv);
CPPUNIT_ASSERT_EQUAL(BHttpMethod(BHttpMethod::Options).Method(), "OPTIONS"sv);
CPPUNIT_ASSERT_EQUAL(BHttpMethod(BHttpMethod::Trace).Method(), "TRACE"sv);
}
// Valid custom method
{
try {
auto method = BHttpMethod("PATCH"sv);
CPPUNIT_ASSERT_EQUAL(method.Method(), "PATCH"sv);
} catch (...) {
CPPUNIT_FAIL("Unexpected error when creating valid method");
}
}
// Invalid empty method
try {
auto method = BHttpMethod("");
CPPUNIT_FAIL("Creating an empty method was succesful unexpectedly");
} catch (BHttpMethod::InvalidMethod&) {
// success
} catch (...) {
CPPUNIT_FAIL("Unexpected exception type when creating an empty method");
}
// Method with invalid characters (arabic translation of GET)
try {
auto method = BHttpMethod("جلب");
CPPUNIT_FAIL("Creating a method with invalid characters was succesful unexpectedly");
} catch (BHttpMethod::InvalidMethod&) {
// success
} catch (...) {
CPPUNIT_FAIL("Unexpected exception type when creating a method with invalid characters");
}
}
/* static */ void
HttpProtocolTest::AddTests(BTestSuite& parent)
{
@ -175,6 +226,8 @@ HttpProtocolTest::AddTests(BTestSuite& parent)
suite.addTest(new CppUnit::TestCaller<HttpProtocolTest>(
"HttpProtocolTest::HttpFieldsTest", &HttpProtocolTest::HttpFieldsTest));
suite.addTest(new CppUnit::TestCaller<HttpProtocolTest>(
"HttpProtocolTest::HttpMethodTest", &HttpProtocolTest::HttpMethodTest));
parent.addTest("HttpProtocolTest", &suite);
}

View File

@ -18,6 +18,7 @@ public:
HttpProtocolTest();
void HttpFieldsTest();
void HttpMethodTest();
static void AddTests(BTestSuite& suite);