From f9d9d20245dbd810b02529a10c9b95a1d3055701 Mon Sep 17 00:00:00 2001 From: Niels Sascha Reedijk Date: Sun, 29 May 2022 15:13:54 +0100 Subject: [PATCH] NetServices: Add support for Basic authentication Change-Id: I304104f7096fd935212e1bfa3e988e7945cb5cec --- docs/user/netservices/HttpRequest.dox | 55 +++++++++++++++++++ headers/private/netservices2/HttpRequest.h | 8 +++ .../private/netservices2/NetServicesDefs.h | 3 + .../network/libnetservices2/HttpRequest.cpp | 51 +++++++++++++++-- .../libnetservices2/NetServicesMisc.cpp | 46 ++++++++++++++++ .../net/netservices2/HttpProtocolTest.cpp | 19 +++++++ .../kits/net/netservices2/HttpProtocolTest.h | 1 + 7 files changed, 179 insertions(+), 4 deletions(-) diff --git a/docs/user/netservices/HttpRequest.dox b/docs/user/netservices/HttpRequest.dox index 575ba74b7d..ab05f5783d 100644 --- a/docs/user/netservices/HttpRequest.dox +++ b/docs/user/netservices/HttpRequest.dox @@ -308,6 +308,34 @@ namespace Network { */ +/*! + \struct BHttpAuthentication + \ingroup netservices + \brief Describe username and password for basic authentication for the request. + + \see These options are used by \ref BHttpRequest::Authentication() and + \ref BHttpRequest::SetAuthentication() + + \since Haiku R1 +*/ + + +/*! + \var BString BHttpAuthentication::username + \brief The username for the request. + + \since Haiku R1 +*/ + + +/*! + \var BString BHttpAuthentication::password + \brief The password for the request. + + \since Haiku R1 +*/ + + /*! \class BHttpRequest \ingroup netservices @@ -478,6 +506,18 @@ namespace Network { //! @{ +/*! + \fn const BHttpAuthentication* BHttpRequest::Authentication() const noexcept + \brief Get the credentials for the authentication for the request. + + \return When no credentials are set for this request, the method returns a \c nullptr. + Otherwise, it will return a pointer to the current BHttpAuthentication data set for this + request. + + \since Haiku R1 +*/ + + /*! \fn const BHttpFields& BHttpRequest::Fields() const noexcept \brief Get the additional header fields set for the request. @@ -531,6 +571,21 @@ namespace Network { //! @{ +/*! + \fn void BHttpRequest::SetAuthentication(const BHttpAuthentication &authentication) + \brief Set the credentials to enable basic authentication for the request. + + The Basic authorization line is added to the request upon setting the request details. There is + no support for other authentication schemes, like digest authentication. + + \param authentication The credentials to apply to the request. + + \exception std::bad_alloc This exception may be raised if it is impossible to allocate memory. + + \since Haiku R1 +*/ + + /*! \fn void BHttpRequest::SetFields(const BHttpFields &fields) \brief Set additional header \a fields for this request. diff --git a/headers/private/netservices2/HttpRequest.h b/headers/private/netservices2/HttpRequest.h index fe1c905f00..95f55a6ff2 100644 --- a/headers/private/netservices2/HttpRequest.h +++ b/headers/private/netservices2/HttpRequest.h @@ -80,6 +80,12 @@ struct BHttpRedirectOptions { }; +struct BHttpAuthentication { + BString username; + BString password; +}; + + class BHttpRequest { public: // Constructors and Destructor @@ -95,12 +101,14 @@ public: // Access bool IsEmpty() const noexcept; + const BHttpAuthentication* Authentication() const noexcept; const BHttpFields& Fields() const noexcept; const BHttpMethod& Method() const noexcept; const BHttpRedirectOptions& Redirect() const noexcept; const BUrl& Url() const noexcept; // Named Setters + void SetAuthentication(const BHttpAuthentication& authentication); void SetFields(const BHttpFields& fields); void SetMethod(const BHttpMethod& method); void SetRedirect(const BHttpRedirectOptions& redirectOptions); diff --git a/headers/private/netservices2/NetServicesDefs.h b/headers/private/netservices2/NetServicesDefs.h index 2b09c7a90c..88894d64d4 100644 --- a/headers/private/netservices2/NetServicesDefs.h +++ b/headers/private/netservices2/NetServicesDefs.h @@ -75,6 +75,9 @@ private: }; +BString encode_to_base64(const BString& string); + + } } diff --git a/src/kits/network/libnetservices2/HttpRequest.cpp b/src/kits/network/libnetservices2/HttpRequest.cpp index 07e8fb6e9b..8f3dd7ec05 100644 --- a/src/kits/network/libnetservices2/HttpRequest.cpp +++ b/src/kits/network/libnetservices2/HttpRequest.cpp @@ -160,13 +160,30 @@ static const BHttpRedirectOptions kDefaultRedirectOptions = BHttpRedirectOptions static const BHttpFields kDefaultOptionalFields = BHttpFields(); struct BHttpRequest::Data { - BUrl url = kDefaultUrl; - BHttpMethod method = kDefaultMethod; - BHttpRedirectOptions redirectOptions; - BHttpFields optionalFields; + BUrl url = kDefaultUrl; + BHttpMethod method = kDefaultMethod; + BHttpRedirectOptions redirectOptions; + BHttpFields optionalFields; + std::optional authentication; }; +// #pragma mark -- BHttpRequest helper functions + + +/*! + \brief Build basic authentication header +*/ +static inline BString +build_basic_http_header(const BString& username, const BString& password) +{ + BString basicEncode, result; + basicEncode << username << ":" << password; + result << "Basic " << encode_to_base64(basicEncode); + return result; +} + + // #pragma mark -- BHttpRequest @@ -201,6 +218,15 @@ BHttpRequest::IsEmpty() const noexcept } +const BHttpAuthentication* +BHttpRequest::Authentication() const noexcept +{ + if (fData && fData->authentication) + return std::addressof(*fData->authentication); + return nullptr; +} + + const BHttpFields& BHttpRequest::Fields() const noexcept { @@ -237,6 +263,16 @@ BHttpRequest::Url() const noexcept } +void +BHttpRequest::SetAuthentication(const BHttpAuthentication& authentication) +{ + if (!fData) + fData = std::make_unique(); + + fData->authentication = authentication; +} + + static constexpr std::array fReservedOptionalFieldNames = { "Host"sv, "Accept"sv, @@ -357,6 +393,13 @@ BHttpRequest::SerializeHeaderTo(BDataIO* target) const }); } + if (fData->authentication) { + // This request will add a Basic authorization header + BString authorization = build_basic_http_header(fData->authentication->username, + fData->authentication->password); + outputFields.AddField("Authorization"sv, std::string_view(authorization.String())); + } + for (const auto& field: outputFields) { bytesWritten += _write_to_dataio(target, field.RawField()); bytesWritten += _write_to_dataio(target, "\r\n"sv); diff --git a/src/kits/network/libnetservices2/NetServicesMisc.cpp b/src/kits/network/libnetservices2/NetServicesMisc.cpp index 4c37776a07..5cef25ae32 100644 --- a/src/kits/network/libnetservices2/NetServicesMisc.cpp +++ b/src/kits/network/libnetservices2/NetServicesMisc.cpp @@ -152,6 +152,52 @@ BNetworkRequestError::ErrorCode() const noexcept } +// #pragma mark -- Public functions + + +static const char* kBase64Symbols + = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + + +BString +encode_to_base64(const BString& string) +{ + BString result; + BString tmpString = string; + + while (tmpString.Length()) { + char in[3] = { 0, 0, 0 }; + char out[4] = { 0, 0, 0, 0 }; + int8 remaining = tmpString.Length(); + + tmpString.MoveInto(in, 0, 3); + + out[0] = (in[0] & 0xFC) >> 2; + out[1] = ((in[0] & 0x03) << 4) | ((in[1] & 0xF0) >> 4); + out[2] = ((in[1] & 0x0F) << 2) | ((in[2] & 0xC0) >> 6); + out[3] = in[2] & 0x3F; + + for (int i = 0; i < 4; i++) + out[i] = kBase64Symbols[(int)out[i]]; + + // Add padding if the input length is not a multiple + // of 3 + switch (remaining) { + case 1: + out[2] = '='; + // Fall through + case 2: + out[3] = '='; + break; + } + + result.Append(out, 4); + } + + return result; +} + + // #pragma mark -- Private functions and data diff --git a/src/tests/kits/net/netservices2/HttpProtocolTest.cpp b/src/tests/kits/net/netservices2/HttpProtocolTest.cpp index 533c190b56..9c0795735d 100644 --- a/src/tests/kits/net/netservices2/HttpProtocolTest.cpp +++ b/src/tests/kits/net/netservices2/HttpProtocolTest.cpp @@ -494,6 +494,7 @@ HttpIntegrationTest::AddTests(BTestSuite& parent) testCaller->addThread("HeadTest", &HttpIntegrationTest::HeadTest); testCaller->addThread("NoContentTest", &HttpIntegrationTest::NoContentTest); testCaller->addThread("AutoRedirectTest", &HttpIntegrationTest::AutoRedirectTest); + testCaller->addThread("BasicAuthTest", &HttpIntegrationTest::BasicAuthTest); suite.addTest(testCaller); parent.addTest("HttpIntegrationTest", &suite); @@ -514,6 +515,7 @@ HttpIntegrationTest::AddTests(BTestSuite& parent) testCaller->addThread("HeadTest", &HttpIntegrationTest::HeadTest); testCaller->addThread("NoContentTest", &HttpIntegrationTest::NoContentTest); testCaller->addThread("AutoRedirectTest", &HttpIntegrationTest::AutoRedirectTest); + testCaller->addThread("BasicAuthTest", &HttpIntegrationTest::BasicAuthTest); suite.addTest(testCaller); parent.addTest("HttpsIntegrationTest", &suite); @@ -680,3 +682,20 @@ HttpIntegrationTest::AutoRedirectTest() CPPUNIT_FAIL(e.DebugMessage().String()); } } + + +void +HttpIntegrationTest::BasicAuthTest() +{ + // Basic Authentication + auto request = BHttpRequest(BUrl(fTestServer.BaseUrl(), "/auth/basic/walter/secret")); + request.SetAuthentication({"walter", "secret"}); + auto result = fSession.Execute(std::move(request)); + CPPUNIT_ASSERT(result.Status().code == 200); + + // Basic Authentication with incorrect credentials + request = BHttpRequest(BUrl(fTestServer.BaseUrl(), "/auth/basic/walter/secret")); + request.SetAuthentication({"invaliduser", "invalidpassword"}); + result = fSession.Execute(std::move(request)); + CPPUNIT_ASSERT(result.Status().code == 401); +} diff --git a/src/tests/kits/net/netservices2/HttpProtocolTest.h b/src/tests/kits/net/netservices2/HttpProtocolTest.h index 2a4872f5e1..eaed90726b 100644 --- a/src/tests/kits/net/netservices2/HttpProtocolTest.h +++ b/src/tests/kits/net/netservices2/HttpProtocolTest.h @@ -43,6 +43,7 @@ public: void HeadTest(); void NoContentTest(); void AutoRedirectTest(); + void BasicAuthTest(); static void AddTests(BTestSuite& suite);