NetServices: Introduce BHttpRequestStream and abstract interface.

This supports asynchronous transfers of Http Requests to a network
interface.

Change-Id: I845fb2e08160d219f85b7a08d2d8872ac7359b47
This commit is contained in:
Niels Sascha Reedijk 2022-04-03 08:49:05 +01:00
parent d9a4c6070c
commit 02ea57d7f9
7 changed files with 359 additions and 6 deletions

View File

@ -0,0 +1,158 @@
/*
* 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/HttpStream.h hrev?????
* src/kits/network/libnetservices2/HttpStream.cpp hrev?????
*/
#if __cplusplus >= 201703L
/*!
\file HttpStream.h
\ingroup netservices
\brief Provides classes and tools to stream HTTP requests and responses.
\since Haiku R1
*/
namespace BPrivate {
namespace Network {
/*!
\class BAbstractDataStream
\ingroup netservices
\brief Abstract interface for adaptors that incrementally streams HTTP requests or responses.
While this interface, and the adapters that implement it, are part of the public API, it will
be unnecessary for most intents and purposes to use them in your application. They are used
internally, by the session classes like \ref BHttpSession.
\since Haiku R1
*/
/*!
\struct BAbstractDataStream::TransferInfo
\ingroup netservices
\brief Data type to describe the progress of a transfer in a stream.
\since Haiku R1
*/
/*!
\var bool BAbstractDataStream::TransferInfo::complete
\brief Set to \c true if the data transmission is complete, or \c false if there is more to be
transferred.
\since Haiku R1
*/
/*!
\var off_t BAbstractDataStream::TransferInfo::currentBytesWritten
\brief Set to the number of bytes that was written or read in the most recent call of the
\ref BAbstractDataStream::Transfer() call.
\since Haiku R1
*/
/*!
\var off_t BAbstractDataStream::TransferInfo::totalBytesWritten
\brief Set to the total number of bytes that was written or read in all the calls of the
\ref BAbstractDataStream::Transfer() method.
\since Haiku R1
*/
/*!
\var off_t BAbstractDataStream::TransferInfo::totalSize
\brief Set to the total number of bytes that will be written or read as part of this
stream. It may be set to \c -1 if this is unknown.
\since Haiku R1
*/
/*!
\fn virtual TransferInfo BAbstractDataStream::Transfer(BDataIO *)=0
\brief Transfer the next set of bytes to and from the data stream.
Implementations of this interface should provide this method. It should send or receive from
the \ref BDataIO argument interface. When implementing this method, consider that it will be
used in asynchronous data transfers. This means:
- It should expect the IO's Read/Write calls to return \c B_WOULD_BLOCK. In that case the
data stream will be paused until the next invocation of this method.
- The implementation needs to be stateful, so that multiple calls to this method will
incrementally complete the transfer.
\return The actual progress info of the transfer.
\since Haiku R1
*/
/*!
\class BHttpRequestStream
\ingroup netservices
\brief Stream a \ref BHttpRequest to an IO output.
\note While this class is part of the public API, it will be unnecessary for most intents and
purposes to use them in your application. They are used internally by \ref BHttpSession.
\since Haiku R1
*/
/*!
\fn BPrivate::Network::BHttpRequestStream::BHttpRequestStream(const BHttpRequest &request)
\brief Constructor
The lifetime of the object is bound to the lifetime of the \a request object. While the
request is being streamed, the \a request object should not be altered, as this may lead to
an invalid request being streamed to the network.
\param request The BHttpRequest to stream.
\since Haiku R1
*/
/*!
\fn BHttpRequestStream::~BHttpRequestStream()
\brief Destructor
\since Haiku R1
*/
/*!
\fn virtual TransferInfo BHttpRequestStream::Transfer(BDataIO *target) override
\brief Stream the HTTP request to the \a target.
\param target The IO object to transfer the request to.
\return The actual progress info of the transfer.
\since Haiku R1
*/
} // namespace Network
} // namespace BPrivate
#endif

View File

@ -33,7 +33,6 @@ public:
BHttpSession& operator=(const BHttpSession&) noexcept;
BHttpSession& operator=(BHttpSession&&) noexcept = delete;
// Requests
BHttpResult Execute(BHttpRequest&& request,
std::unique_ptr<BDataIO> target = nullptr,
@ -46,8 +45,8 @@ private:
};
}
} // namespace Network
}
} // namespace BPrivate
#endif // _B_HTTP_REQUEST_H_
#endif // _B_HTTP_SESSION_H_

View File

@ -0,0 +1,56 @@
/*
* Copyright 2022 Haiku Inc. All rights reserved.
* Distributed under the terms of the MIT License.
*/
#ifndef _HTTP_STREAM_H_
#define _HTTP_STREAM_H_
#include <memory>
class BDataIO;
class BMallocIO;
class BString;
namespace BPrivate {
namespace Network {
class BHttpRequest;
class BAbstractDataStream {
public:
struct TransferInfo {
off_t currentBytesWritten;
off_t totalBytesWritten;
off_t totalSize;
bool complete;
};
virtual TransferInfo Transfer(BDataIO*) = 0;
};
class BHttpRequestStream : public BAbstractDataStream {
public:
BHttpRequestStream(const BHttpRequest& request);
~BHttpRequestStream();
virtual TransferInfo Transfer(BDataIO* target) override;
private:
std::unique_ptr<BMallocIO> fHeader;
BDataIO* fBody;
off_t fTotalSize = 0;
off_t fBodyOffset = 0;
off_t fCurrentPos = 0;
};
} // namespace Network
} // namespace BPrivate
#endif // _HTTP_STREAM_H_

View File

@ -0,0 +1,61 @@
/*
* Copyright 2022 Haiku Inc. All rights reserved.
* Distributed under the terms of the MIT License.
*
* Authors:
* Niels Sascha Reedijk, niels.reedijk@gmail.com
*/
#include <HttpStream.h>
#include <DataIO.h>
#include <HttpRequest.h>
using namespace BPrivate::Network;
BHttpRequestStream::BHttpRequestStream(const BHttpRequest& request)
: fHeader(std::make_unique<BMallocIO>()), fBody(nullptr)
{
// Serialize the header of the request to text
fTotalSize = request.SerializeHeaderTo(fHeader.get());
fBodyOffset = fTotalSize;
// TODO: add size of request body to total size
}
BHttpRequestStream::~BHttpRequestStream() = default;
BHttpRequestStream::TransferInfo
BHttpRequestStream::Transfer(BDataIO* target)
{
if (fCurrentPos == fTotalSize)
return TransferInfo{0, fTotalSize, fTotalSize, true};
off_t bytesWritten = 0;
if (fCurrentPos < fBodyOffset) {
// Writing the header
auto remainingSize = fBodyOffset - fCurrentPos;
bytesWritten = target->Write(
static_cast<const char*>(fHeader->Buffer()) + fCurrentPos, remainingSize);
if (bytesWritten == B_WOULD_BLOCK)
return TransferInfo{0, 0, fTotalSize, false};
else if (bytesWritten < 0)
throw BSystemError("BDataIO::Write()", bytesWritten);
fCurrentPos += bytesWritten;
if (bytesWritten < remainingSize) {
return TransferInfo{bytesWritten, fCurrentPos, fTotalSize, false};
}
}
// Write the body
if (fBody) {
// TODO
throw BRuntimeError(__PRETTY_FUNCTION__, "Not implemented");
}
return TransferInfo{bytesWritten, fTotalSize, fTotalSize, true};
}

View File

@ -23,6 +23,7 @@ for architectureObject in [ MultiArchSubDirSetup ] {
HttpRequest.cpp
HttpResult.cpp
HttpSession.cpp
HttpStream.cpp
NetServicesMisc.cpp
;

View File

@ -15,14 +15,16 @@
#include <HttpFields.h>
#include <HttpRequest.h>
#include <HttpResult.h>
#include <HttpStream.h>
#include <NetServicesDefs.h>
#include <Url.h>
using BPrivate::Network::BHttpFields;
using BPrivate::Network::BHttpMethod;
using BPrivate::Network::BHttpRequest;
using BPrivate::Network::BHttpSession;
using BPrivate::Network::BHttpResult;
using BPrivate::Network::BHttpSession;
using BPrivate::Network::BHttpRequestStream;
using BPrivate::Network::BNetworkRequestError;
@ -225,6 +227,14 @@ HttpProtocolTest::HttpMethodTest()
}
constexpr std::string_view kHaikuGetRequestText =
"GET / HTTP/1.1\r\n"
"Host: www.haiku-os.org\r\n"
"Accept: *\r\n"
"Accept-Encoding: gzip\r\n"
"Connection: close\r\n\r\n";
void
HttpProtocolTest::HttpRequestTest()
{
@ -237,7 +247,72 @@ HttpProtocolTest::HttpRequestTest()
// Validate header serialization
BString header = request.HeaderToString();
CPPUNIT_ASSERT(header.Compare("GET / HTTP/1.1\r\nHost: www.haiku-os.org\r\nAccept: *\r\nAccept-Encoding: gzip\r\nConnection: close\r\n\r\n") == 0);
CPPUNIT_ASSERT(header.Compare(kHaikuGetRequestText.data(), kHaikuGetRequestText.size()) == 0);
}
class RequestStreamTestIO : public BDataIO
{
public:
RequestStreamTestIO(const std::string_view expectedOutput)
: fExpectedOutput(expectedOutput)
{
}
// Accept maximum of 8 bytes at a time.
ssize_t Write(const void* buffer, size_t size) {
ssize_t bytesWritten = (size < 8) ? size : 8;
CPPUNIT_ASSERT_MESSAGE("RequestStreamTestIO: bytes written larger than expected output",
fExpectedOutput.size() >= (fPos + bytesWritten));
CPPUNIT_ASSERT(fExpectedOutput.substr(fPos, bytesWritten) == std::string_view(static_cast<const char*>(buffer), bytesWritten));
fPos += bytesWritten;
return bytesWritten;
};
private:
const std::string_view fExpectedOutput;
ssize_t fPos = 0;
};
void
HttpProtocolTest::HttpRequestStreamTest()
{
// Set up basic GET for https://www.haiku-os.org/
BHttpRequest request;
auto url = BUrl("https://www.haiku-os.org");
request.SetUrl(url);
// Test streaming the request
BHttpRequestStream requestStream(request);
RequestStreamTestIO testIO(kHaikuGetRequestText.data());
bool finished = false;
ssize_t expectedBytesWritten = 8;
ssize_t expectedTotalBytesWritten = 8;
const ssize_t expectedTotalSize = kHaikuGetRequestText.size();
if (expectedTotalSize < 8) {
expectedBytesWritten = expectedTotalSize;
expectedTotalBytesWritten = expectedTotalSize;
}
while (!finished) {
auto [currentBytesWritten, totalBytesWritten, totalSize, complete] = requestStream.Transfer(&testIO);
CPPUNIT_ASSERT_EQUAL(expectedBytesWritten, currentBytesWritten);
CPPUNIT_ASSERT_EQUAL(expectedTotalBytesWritten, totalBytesWritten);
CPPUNIT_ASSERT_EQUAL(expectedTotalSize, totalSize);
if (expectedTotalBytesWritten == expectedTotalSize) {
// loop should be finished
CPPUNIT_ASSERT_EQUAL(true, complete);
finished = true;
} else {
// prepare for next loop
if (totalSize - totalBytesWritten < 8) {
expectedBytesWritten = totalSize - totalBytesWritten;
}
expectedTotalBytesWritten += expectedBytesWritten;
CPPUNIT_ASSERT_EQUAL(false, complete);
}
}
}
@ -300,6 +375,8 @@ HttpProtocolTest::AddTests(BTestSuite& parent)
"HttpProtocolTest::HttpMethodTest", &HttpProtocolTest::HttpMethodTest));
suite.addTest(new CppUnit::TestCaller<HttpProtocolTest>(
"HttpProtocolTest::HttpRequestTest", &HttpProtocolTest::HttpRequestTest));
suite.addTest(new CppUnit::TestCaller<HttpProtocolTest>(
"HttpProtocolTest::HttpRequestStreamTest", &HttpProtocolTest::HttpRequestStreamTest));
suite.addTest(new CppUnit::TestCaller<HttpProtocolTest>(
"HttpProtocolTest::HttpIntegrationTest", &HttpProtocolTest::HttpIntegrationTest));

View File

@ -20,6 +20,7 @@ public:
void HttpFieldsTest();
void HttpMethodTest();
void HttpRequestTest();
void HttpRequestStreamTest();
void HttpIntegrationTest();
static void AddTests(BTestSuite& suite);