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:
@@ -0,0 +1,255 @@
|
|||||||
|
/*
|
||||||
|
* 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/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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 Haiku Inc. All rights reserved.
|
||||||
|
* Distributed under the terms of the MIT License.
|
||||||
|
*
|
||||||
|
* Authors:
|
||||||
|
* Niels Sascha Reedijk, [email protected]
|
||||||
|
*/
|
||||||
|
|
||||||
|
#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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ for architectureObject in [ MultiArchSubDirSetup ] {
|
|||||||
StaticLibrary [ MultiArchDefaultGristFiles libnetservices2.a ] :
|
StaticLibrary [ MultiArchDefaultGristFiles libnetservices2.a ] :
|
||||||
ErrorsExt.cpp
|
ErrorsExt.cpp
|
||||||
HttpFields.cpp
|
HttpFields.cpp
|
||||||
|
HttpRequest.cpp
|
||||||
HttpSession.cpp
|
HttpSession.cpp
|
||||||
NetServicesMisc.cpp
|
NetServicesMisc.cpp
|
||||||
;
|
;
|
||||||
|
|||||||
@@ -13,8 +13,10 @@
|
|||||||
#include <cppunit/TestSuite.h>
|
#include <cppunit/TestSuite.h>
|
||||||
|
|
||||||
#include <HttpFields.h>
|
#include <HttpFields.h>
|
||||||
|
#include <HttpRequest.h>
|
||||||
|
|
||||||
using BPrivate::Network::BHttpFields;
|
using BPrivate::Network::BHttpFields;
|
||||||
|
using BPrivate::Network::BHttpMethod;
|
||||||
using BPrivate::Network::BHttpSession;
|
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
|
/* static */ void
|
||||||
HttpProtocolTest::AddTests(BTestSuite& parent)
|
HttpProtocolTest::AddTests(BTestSuite& parent)
|
||||||
{
|
{
|
||||||
@@ -175,6 +226,8 @@ HttpProtocolTest::AddTests(BTestSuite& parent)
|
|||||||
|
|
||||||
suite.addTest(new CppUnit::TestCaller<HttpProtocolTest>(
|
suite.addTest(new CppUnit::TestCaller<HttpProtocolTest>(
|
||||||
"HttpProtocolTest::HttpFieldsTest", &HttpProtocolTest::HttpFieldsTest));
|
"HttpProtocolTest::HttpFieldsTest", &HttpProtocolTest::HttpFieldsTest));
|
||||||
|
suite.addTest(new CppUnit::TestCaller<HttpProtocolTest>(
|
||||||
|
"HttpProtocolTest::HttpMethodTest", &HttpProtocolTest::HttpMethodTest));
|
||||||
|
|
||||||
parent.addTest("HttpProtocolTest", &suite);
|
parent.addTest("HttpProtocolTest", &suite);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ public:
|
|||||||
HttpProtocolTest();
|
HttpProtocolTest();
|
||||||
|
|
||||||
void HttpFieldsTest();
|
void HttpFieldsTest();
|
||||||
|
void HttpMethodTest();
|
||||||
|
|
||||||
static void AddTests(BTestSuite& suite);
|
static void AddTests(BTestSuite& suite);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user