NetServices: move HttpBuffer and HttpParser into their own header/source

Change-Id: I5bc0d9df6f94c2cf1c39baa6206bf6f1db284705
This commit is contained in:
Niels Sascha Reedijk
2022-08-01 08:11:56 +01:00
parent 9b33a931f1
commit e68284565a
8 changed files with 662 additions and 565 deletions
+1 -71
View File
@@ -23,6 +23,7 @@ namespace Network {
class BHttpFields;
class BHttpRequest;
class BHttpStatus;
class HttpBuffer;
class BAbstractDataStream {
@@ -62,77 +63,6 @@ private:
using HttpTransferFunction = std::function<size_t (const std::byte*, size_t)>;
class HttpBuffer {
public:
HttpBuffer(size_t capacity = 8*1024);
ssize_t ReadFrom(BDataIO* source);
void WriteExactlyTo(BDataIO* target);
void WriteTo(HttpTransferFunction func,
std::optional<size_t> maxSize = std::nullopt);
std::optional<BString> GetNextLine();
size_t RemainingBytes() noexcept;
void Flush() noexcept;
void Clear() noexcept;
private:
std::vector<std::byte> fBuffer;
size_t fCurrentOffset = 0;
};
enum class HttpBodyInputStreamState {
ChunkSize,
ChunkEnd,
Chunk,
Trailers,
Done
};
class HttpParser {
public:
HttpParser() {};
// HTTP Header
bool ParseStatus(HttpBuffer& buffer, BHttpStatus& status);
bool ParseFields(HttpBuffer& buffer, BHttpFields& fields);
// HTTP Body
void SetGzipCompression(bool compression = true);
void SetContentLength(std::optional<off_t> contentLength) noexcept;
size_t ParseBody(HttpBuffer& buffer, HttpTransferFunction writeToBody);
std::optional<off_t> BodyBytesTotal() const noexcept { return fBodyBytesTotal; };
off_t BodyBytesTransferred() const noexcept { return fTransferredBodySize; };
bool Complete() const noexcept;
private:
size_t _ParseBodyRaw(HttpBuffer& buffer, HttpTransferFunction writeToBody);
size_t _ParseBodyChunked(HttpBuffer& buffer, HttpTransferFunction writeToBody);
size_t _ReadChunk(HttpBuffer& buffer, HttpTransferFunction writeToBody,
size_t maxSize, bool flush);
bool _IsChunked() const noexcept;
private:
off_t fHeaderBytes = 0;
// Support for chunked transfers
HttpBodyInputStreamState fBodyState = HttpBodyInputStreamState::ChunkSize;
off_t fRemainingChunkSize = 0;
bool fLastChunk = false;
// Receive stats
std::optional<off_t> fBodyBytesTotal = 0;
off_t fTransferredBodySize = 0;
// Optional decompression
std::unique_ptr<BMallocIO> fDecompressorStorage = nullptr;
std::unique_ptr<BDataIO> fDecompressingStream = nullptr;
};
} // namespace Network
@@ -0,0 +1,179 @@
/*
* Copyright 2022 Haiku Inc. All rights reserved.
* Distributed under the terms of the MIT License.
*
* Authors:
* Niels Sascha Reedijk, [email protected]
*/
#include "HttpBuffer.h"
#include <DataIO.h>
#include <NetServicesDefs.h>
#include <String.h>
using namespace BPrivate::Network;
/*!
\brief Newline sequence
As per the RFC, defined as \r\n
*/
static constexpr std::array<std::byte, 2> kNewLine = {std::byte('\r'), std::byte('\n')};
/*!
\brief Create a new HTTP buffer with \a capacity.
*/
HttpBuffer::HttpBuffer(size_t capacity)
{
fBuffer.reserve(capacity);
};
/*!
\brief Load data from \a source into the spare capacity of this buffer.
\exception BNetworkRequestError When BDataIO::Read() returns any error other than B_WOULD_BLOCK
\retval B_WOULD_BLOCK The read call on the \a source was unsuccessful because it would block.
\retval >=0 The actual number of bytes read.
*/
ssize_t
HttpBuffer::ReadFrom(BDataIO* source)
{
// Remove any unused bytes at the beginning of the buffer
Flush();
auto currentSize = fBuffer.size();
auto remainingBufferSize = fBuffer.capacity() - currentSize;
// Adjust the buffer to the maximum size
fBuffer.resize(fBuffer.capacity());
ssize_t bytesRead = B_INTERRUPTED;
while (bytesRead == B_INTERRUPTED)
bytesRead = source->Read(fBuffer.data() + currentSize, remainingBufferSize);
if (bytesRead == B_WOULD_BLOCK || bytesRead == 0) {
fBuffer.resize(currentSize);
return bytesRead;
} else if (bytesRead < 0) {
throw BNetworkRequestError("BDataIO::Read()", BNetworkRequestError::NetworkError,
bytesRead);
}
// Adjust the buffer to the current size
fBuffer.resize(currentSize + bytesRead);
return bytesRead;
}
/*!
\brief Use BDataIO::WriteExactly() on target to write the contents of the buffer.
*/
void
HttpBuffer::WriteExactlyTo(BDataIO* target)
{
if (RemainingBytes() == 0)
return;
auto status = target->WriteExactly(fBuffer.data() + fCurrentOffset, RemainingBytes());
if (status != B_OK) {
throw BNetworkRequestError("BDataIO::WriteExactly()", BNetworkRequestError::SystemError,
status);
}
// Entire buffer is written; reset internal buffer
Clear();
}
/*!
\brief Write the contents of the buffer through the helper \a func.
\param func Handle the actual writing. The function accepts a pointer and a size as inputs
and should return the number of actual written bytes, which may be fewer than the number
of available bytes.
*/
void
HttpBuffer::WriteTo(HttpTransferFunction func , std::optional<size_t> maxSize)
{
if (RemainingBytes() == 0)
return;
auto size = RemainingBytes();
if (maxSize.has_value() && *maxSize < size)
size = *maxSize;
auto bytesWritten = func(fBuffer.data() + fCurrentOffset, size);
if (bytesWritten > size)
throw BRuntimeError(__PRETTY_FUNCTION__, "More bytes written than were made available");
fCurrentOffset += bytesWritten;
}
/*!
\brief Get the next line from this buffer.
This can be called iteratively until all lines in the current data are read. After using this
method, you should use Flush() to make sure that the read lines are cleared from the beginning
of the buffer.
\retval std::nullopt There are no more lines in the buffer.
\retval BString The next line.
*/
std::optional<BString>
HttpBuffer::GetNextLine()
{
auto offset = fBuffer.cbegin() + fCurrentOffset;
auto result = std::search(offset, fBuffer.cend(), kNewLine.cbegin(), kNewLine.cend());
if (result == fBuffer.cend())
return std::nullopt;
BString line(reinterpret_cast<const char*>(std::addressof(*offset)), std::distance(offset, result));
fCurrentOffset = std::distance(fBuffer.cbegin(), result) + 2;
return line;
}
/*!
\brief Get the number of remaining bytes in this buffer.
*/
size_t
HttpBuffer::RemainingBytes() noexcept
{
return fBuffer.size() - fCurrentOffset;
}
/*!
\brief Move data to the beginning of the buffer to clear at the back.
The GetNextLine() increases the offset of the internal buffer. This call moves remaining data
to the beginning of the buffer sets the correct size, making the remainder of the capacity
available for further reading.
*/
void
HttpBuffer::Flush() noexcept
{
if (fCurrentOffset > 0) {
auto end = fBuffer.cbegin() + fCurrentOffset;
fBuffer.erase(fBuffer.cbegin(), end);
fCurrentOffset = 0;
}
}
/*!
\brief Clear the internal buffer
*/
void
HttpBuffer::Clear() noexcept
{
fBuffer.clear();
fCurrentOffset = 0;
}
@@ -0,0 +1,49 @@
/*
* Copyright 2022 Haiku Inc. All rights reserved.
* Distributed under the terms of the MIT License.
*/
#ifndef _B_HTTP_BUFFER_H_
#define _B_HTTP_BUFFER_H_
#include <functional>
#include <optional>
#include <vector>
class BDataIO;
class BString;
namespace BPrivate {
namespace Network {
using HttpTransferFunction = std::function<size_t (const std::byte*, size_t)>;
class HttpBuffer {
public:
HttpBuffer(size_t capacity = 8*1024);
ssize_t ReadFrom(BDataIO* source);
void WriteExactlyTo(BDataIO* target);
void WriteTo(HttpTransferFunction func,
std::optional<size_t> maxSize = std::nullopt);
std::optional<BString> GetNextLine();
size_t RemainingBytes() noexcept;
void Flush() noexcept;
void Clear() noexcept;
private:
std::vector<std::byte> fBuffer;
size_t fCurrentOffset = 0;
};
} // namespace Network
} // namespace BPrivate
#endif // _B_HTTP_BUFFER_H_
@@ -0,0 +1,345 @@
/*
* Copyright 2022 Haiku Inc. All rights reserved.
* Distributed under the terms of the MIT License.
*
* Authors:
* Niels Sascha Reedijk, [email protected]
*/
#include "HttpParser.h"
#include <string>
#include <stdexcept>
#include <HttpFields.h>
#include <NetServicesDefs.h>
#include <ZlibCompressionAlgorithm.h>
using namespace BPrivate::Network;
/*!
\brief Parse the status from the \a buffer and store it in \a status.
\retval true The status was succesfully parsed
\retval false There is not enough data in the buffer for a full status.
\exception BNetworkRequestException The status does not conform to the HTTP spec.
*/
bool
HttpParser::ParseStatus(HttpBuffer& buffer, BHttpStatus& status)
{
auto statusLine = buffer.GetNextLine();
if (!statusLine)
return false;
auto codeStart = statusLine->FindFirst(' ') + 1;
if (codeStart < 0)
throw BNetworkRequestError(__PRETTY_FUNCTION__, BNetworkRequestError::ProtocolError);
auto codeEnd = statusLine->FindFirst(' ', codeStart);
if (codeEnd < 0 || (codeEnd - codeStart) != 3)
throw BNetworkRequestError(__PRETTY_FUNCTION__, BNetworkRequestError::ProtocolError);
std::string statusCodeString(statusLine->String() + codeStart, 3);
// build the output
try {
status.code = std::stol(statusCodeString);
} catch (...) {
throw BNetworkRequestError(__PRETTY_FUNCTION__, BNetworkRequestError::ProtocolError);
}
status.text = std::move(statusLine.value());
return true;
}
/*!
\brief Parse the fields from the \a buffer and store it in \a fields.
The fields are parsed incrementally, meaning that even if the full header is not yet in the
\a buffer, it will still parse all complete fields and store them in the \a fields.
\retval true All fields were succesfully parsed
\retval false There is not enough data in the buffer to complete parsing of fields.
\exception BNetworkRequestException The fields not conform to the HTTP spec.
*/
bool
HttpParser::ParseFields(HttpBuffer& buffer, BHttpFields& fields)
{
auto fieldLine = buffer.GetNextLine();
while (fieldLine && !fieldLine.value().IsEmpty()){
// Parse next header line
fields.AddField(fieldLine.value());
fieldLine = buffer.GetNextLine();
}
if (fieldLine && fieldLine.value().IsEmpty()){
// end of the header section of the message
return true;
} else {
// there is more to parse
return false;
}
}
/*!
\brief Parse the Gzip compression from the body.
\exception std::bad_alloc in case there is an error allocating memory.
*/
void
HttpParser::SetGzipCompression(bool compression)
{
if (compression) {
fDecompressorStorage = std::make_unique<BMallocIO>();
BDataIO* stream = nullptr;
auto result = BZlibCompressionAlgorithm()
.CreateDecompressingOutputStream(fDecompressorStorage.get(), nullptr, stream);
if (result != B_OK) {
throw BNetworkRequestError(
"BZlibCompressionAlgorithm().CreateCompressingOutputStream",
BNetworkRequestError::SystemError, result);
}
fDecompressingStream = std::unique_ptr<BDataIO>(stream);
} else {
fDecompressingStream = nullptr;
fDecompressorStorage = nullptr;
}
}
/*!
\brief Set the content length of the body.
If a content length is set, the body will not be handled as a chunked transfer.
*/
void
HttpParser::SetContentLength(std::optional<off_t> contentLength) noexcept
{
fBodyBytesTotal = contentLength;
}
/*!
\brief Parse the body from the \a buffer and use \a writeToBody function to save.
*/
size_t
HttpParser::ParseBody(HttpBuffer& buffer, HttpTransferFunction writeToBody)
{
if (fBodyBytesTotal.has_value()) {
return _ParseBodyRaw(buffer, writeToBody);
} else {
return _ParseBodyChunked(buffer, writeToBody);
}
}
/*!
\brief Parse the body from the \a buffer and use \a writeToBody function to save.
*/
size_t
HttpParser::_ParseBodyRaw(HttpBuffer& buffer, HttpTransferFunction writeToBody)
{
if (fBodyBytesTotal && (fTransferredBodySize + static_cast<off_t>(buffer.RemainingBytes()))
> *fBodyBytesTotal)
throw BNetworkRequestError(__PRETTY_FUNCTION__, BNetworkRequestError::ProtocolError);
auto bytesToRead = buffer.RemainingBytes();
auto readEnd = fBodyBytesTotal.value()
== (fTransferredBodySize + static_cast<off_t>(bytesToRead));
auto bytesRead = _ReadChunk(buffer, writeToBody, bytesToRead, readEnd);
fTransferredBodySize += bytesRead;
return bytesRead;
}
/*!
\brief Parse the body from the \a buffer and use \a writeToBody function to save.
*/
size_t
HttpParser::_ParseBodyChunked(HttpBuffer& buffer, HttpTransferFunction writeToBody)
{
size_t totalBytesRead = 0;
while (buffer.RemainingBytes() > 0) {
switch (fBodyState) {
case HttpBodyInputStreamState::ChunkSize:
{
// Read the next chunk size from the buffer; if unsuccesful wait for more data
auto chunkSizeString = buffer.GetNextLine();
if (!chunkSizeString)
return totalBytesRead;
auto chunkSizeStr = std::string(chunkSizeString.value().String());
try {
size_t pos = 0;
fRemainingChunkSize = std::stoll(chunkSizeStr, &pos, 16);
if (pos < chunkSizeStr.size() && chunkSizeStr[pos] != ';'){
throw BNetworkRequestError(__PRETTY_FUNCTION__,
BNetworkRequestError::ProtocolError);
}
} catch (const std::invalid_argument&) {
throw BNetworkRequestError(__PRETTY_FUNCTION__,
BNetworkRequestError::ProtocolError);
} catch (const std::out_of_range&) {
throw BNetworkRequestError(__PRETTY_FUNCTION__,
BNetworkRequestError::ProtocolError);
}
if (fRemainingChunkSize > 0)
fBodyState = HttpBodyInputStreamState::Chunk;
else
fBodyState = HttpBodyInputStreamState::Trailers;
break;
}
case HttpBodyInputStreamState::Chunk:
{
size_t bytesToRead;
bool readEnd = false;
if (fRemainingChunkSize > static_cast<off_t>(buffer.RemainingBytes()))
bytesToRead = buffer.RemainingBytes();
else {
readEnd = true;
bytesToRead = fRemainingChunkSize;
}
auto bytesRead = _ReadChunk(buffer, writeToBody, bytesToRead, readEnd);
fTransferredBodySize += bytesRead;
totalBytesRead += bytesRead;
fRemainingChunkSize -= bytesRead;
if (fRemainingChunkSize == 0)
fBodyState = HttpBodyInputStreamState::ChunkEnd;
break;
}
case HttpBodyInputStreamState::ChunkEnd:
{
if (buffer.RemainingBytes() < 2) {
// not enough data in the buffer to finish the chunk
return totalBytesRead;
}
auto chunkEndString = buffer.GetNextLine();
if (!chunkEndString || chunkEndString.value().Length() != 0) {
// There should have been an empty chunk
throw BNetworkRequestError(__PRETTY_FUNCTION__,
BNetworkRequestError::ProtocolError);
}
fBodyState = HttpBodyInputStreamState::ChunkSize;
break;
}
case HttpBodyInputStreamState::Trailers:
{
auto trailerString = buffer.GetNextLine();
if (!trailerString) {
// More data to come
return totalBytesRead;
}
if (trailerString.value().Length() > 0) {
// Ignore empty trailers for now
// TODO: review if the API should support trailing headers
} else {
fBodyState = HttpBodyInputStreamState::Done;
return totalBytesRead;
}
break;
}
case HttpBodyInputStreamState::Done:
return totalBytesRead;
}
}
return totalBytesRead;
}
/*!
\brief Check if the body is fully parsed.
*/
bool
HttpParser::Complete() const noexcept
{
if (_IsChunked())
return fBodyState == HttpBodyInputStreamState::Done;
return fBodyBytesTotal.value() == fTransferredBodySize;
}
/*!
\brief Read a chunk of data from the buffer and write it to the output.
If there is a compression algorithm applied, then it passes through the compression first.
\param buffer The buffer to read from
\param writeToBody The function that can write data from the buffer to the body.
\param size The maximum size to read from the buffer. When larger than the buffer size, the
remaining bytes from the buffer are read.
\param flush Setting this parameter will force the decompression to write out all data, if
applicable. Set when all the data has been received.
\exception BNetworkRequestError When there was any error with any of the system cals.
*/
size_t
HttpParser::_ReadChunk(HttpBuffer& buffer, HttpTransferFunction writeToBody, size_t size, bool flush)
{
if (size == 0)
return 0;
if (size > buffer.RemainingBytes())
size = buffer.RemainingBytes();
if (fDecompressingStream) {
buffer.WriteTo([this](const std::byte* buffer, size_t bufferSize){
auto status = fDecompressingStream->WriteExactly(buffer, bufferSize);
if (status != B_OK) {
throw BNetworkRequestError("BDataIO::WriteExactly()",
BNetworkRequestError::SystemError, status);
}
return bufferSize;
}, size);
if (flush) {
// No more bytes expected so flush out the final bytes
if (auto status = fDecompressingStream->Flush(); status != B_OK)
throw BNetworkRequestError("BZlibDecompressionStream::Flush()",
BNetworkRequestError::SystemError, status);
}
if (auto bodySize = fDecompressorStorage->Position(); bodySize > 0) {
auto bytesWritten
= writeToBody(static_cast<const std::byte*>(fDecompressorStorage->Buffer()),
bodySize);
if (static_cast<off_t>(bytesWritten) != bodySize) {
throw BNetworkRequestError(__PRETTY_FUNCTION__,
BNetworkRequestError::SystemError, B_PARTIAL_WRITE);
}
fDecompressorStorage->Seek(0, SEEK_SET);
}
} else {
// Write the body directly to the target
buffer.WriteTo(writeToBody, size);
}
return size;
}
/*!
\brief Internal helper to determine if the body is sent as a chunked transfer.
*/
bool
HttpParser::_IsChunked() const noexcept
{
return fBodyBytesTotal == std::nullopt;
}
@@ -0,0 +1,82 @@
/*
* Copyright 2022 Haiku Inc. All rights reserved.
* Distributed under the terms of the MIT License.
*/
#ifndef _B_HTTP_PARSER_H_
#define _B_HTTP_PARSER_H_
#include <functional>
#include <optional>
#include <HttpResult.h>
#include "HttpBuffer.h"
class BMallocIO;
namespace BPrivate {
namespace Network {
using HttpTransferFunction = std::function<size_t (const std::byte*, size_t)>;
enum class HttpBodyInputStreamState {
ChunkSize,
ChunkEnd,
Chunk,
Trailers,
Done
};
class HttpParser {
public:
HttpParser() {};
// HTTP Header
bool ParseStatus(HttpBuffer& buffer, BHttpStatus& status);
bool ParseFields(HttpBuffer& buffer, BHttpFields& fields);
// HTTP Body
void SetGzipCompression(bool compression = true);
void SetContentLength(std::optional<off_t> contentLength) noexcept;
size_t ParseBody(HttpBuffer& buffer, HttpTransferFunction writeToBody);
std::optional<off_t> BodyBytesTotal() const noexcept { return fBodyBytesTotal; };
off_t BodyBytesTransferred() const noexcept { return fTransferredBodySize; };
bool Complete() const noexcept;
private:
size_t _ParseBodyRaw(HttpBuffer& buffer, HttpTransferFunction writeToBody);
size_t _ParseBodyChunked(HttpBuffer& buffer, HttpTransferFunction writeToBody);
size_t _ReadChunk(HttpBuffer& buffer, HttpTransferFunction writeToBody,
size_t maxSize, bool flush);
bool _IsChunked() const noexcept;
private:
off_t fHeaderBytes = 0;
// Support for chunked transfers
HttpBodyInputStreamState fBodyState = HttpBodyInputStreamState::ChunkSize;
off_t fRemainingChunkSize = 0;
bool fLastChunk = false;
// Receive stats
std::optional<off_t> fBodyBytesTotal = 0;
off_t fTransferredBodySize = 0;
// Optional decompression
std::unique_ptr<BMallocIO> fDecompressorStorage = nullptr;
std::unique_ptr<BDataIO> fDecompressingStream = nullptr;
};
} // namespace Network
} // namespace BPrivate
#endif // _B_HTTP_PARSER_H_
@@ -32,6 +32,8 @@
#include <Socket.h>
#include <ZlibCompressionAlgorithm.h>
#include "HttpBuffer.h"
#include "HttpParser.h"
#include "HttpResultPrivate.h"
#include "NetServicesPrivate.h"
+2 -494
View File
@@ -8,9 +8,7 @@
#include <HttpStream.h>
#include <algorithm>
#include <optional>
#include <stdexcept>
#include <string>
#include <DataIO.h>
@@ -18,7 +16,8 @@
#include <HttpRequest.h>
#include <HttpResult.h>
#include <NetServicesDefs.h>
#include <ZlibCompressionAlgorithm.h>
#include "HttpBuffer.h"
using namespace BPrivate::Network;
@@ -36,14 +35,6 @@ using namespace BPrivate::Network;
static constexpr ssize_t kMaxBufferSize = 8192;
/*!
\brief Newline sequence
As per the RFC, defined as \r\n
*/
static constexpr std::array<std::byte, 2> kNewLine = {std::byte('\r'), std::byte('\n')};
// #pragma mark -- ByteIOHelper base class
@@ -218,486 +209,3 @@ BHttpRequestStream::Transfer(BDataIO* target)
auto complete = fRemainingHeaderSize == 0 && fTransferredBodySize == fTotalBodySize;
return TransferInfo{bytesWritten, fTransferredBodySize, fTotalBodySize, complete};
}
/*!
\brief Create a new HTTP buffer with \a capacity.
*/
HttpBuffer::HttpBuffer(size_t capacity)
{
fBuffer.reserve(capacity);
};
/*!
\brief Load data from \a source into the spare capacity of this buffer.
\exception BNetworkRequestError When BDataIO::Read() returns any error other than B_WOULD_BLOCK
\retval B_WOULD_BLOCK The read call on the \a source was unsuccessful because it would block.
\retval >=0 The actual number of bytes read.
*/
ssize_t
HttpBuffer::ReadFrom(BDataIO* source)
{
// Remove any unused bytes at the beginning of the buffer
Flush();
auto currentSize = fBuffer.size();
auto remainingBufferSize = fBuffer.capacity() - currentSize;
// Adjust the buffer to the maximum size
fBuffer.resize(fBuffer.capacity());
ssize_t bytesRead = B_INTERRUPTED;
while (bytesRead == B_INTERRUPTED)
bytesRead = source->Read(fBuffer.data() + currentSize, remainingBufferSize);
if (bytesRead == B_WOULD_BLOCK || bytesRead == 0) {
fBuffer.resize(currentSize);
return bytesRead;
} else if (bytesRead < 0) {
throw BNetworkRequestError("BDataIO::Read()", BNetworkRequestError::NetworkError,
bytesRead);
}
// Adjust the buffer to the current size
fBuffer.resize(currentSize + bytesRead);
return bytesRead;
}
/*!
\brief Use BDataIO::WriteExactly() on target to write the contents of the buffer.
*/
void
HttpBuffer::WriteExactlyTo(BDataIO* target)
{
if (RemainingBytes() == 0)
return;
auto status = target->WriteExactly(fBuffer.data() + fCurrentOffset, RemainingBytes());
if (status != B_OK) {
throw BNetworkRequestError("BDataIO::WriteExactly()", BNetworkRequestError::SystemError,
status);
}
// Entire buffer is written; reset internal buffer
Clear();
}
/*!
\brief Write the contents of the buffer through the helper \a func.
\param func Handle the actual writing. The function accepts a pointer and a size as inputs
and should return the number of actual written bytes, which may be fewer than the number
of available bytes.
*/
void
HttpBuffer::WriteTo(HttpTransferFunction func , std::optional<size_t> maxSize)
{
if (RemainingBytes() == 0)
return;
auto size = RemainingBytes();
if (maxSize.has_value() && *maxSize < size)
size = *maxSize;
auto bytesWritten = func(fBuffer.data() + fCurrentOffset, size);
if (bytesWritten > size)
throw BRuntimeError(__PRETTY_FUNCTION__, "More bytes written than were made available");
fCurrentOffset += bytesWritten;
}
/*!
\brief Get the next line from this buffer.
This can be called iteratively until all lines in the current data are read. After using this
method, you should use Flush() to make sure that the read lines are cleared from the beginning
of the buffer.
\retval std::nullopt There are no more lines in the buffer.
\retval BString The next line.
*/
std::optional<BString>
HttpBuffer::GetNextLine()
{
auto offset = fBuffer.cbegin() + fCurrentOffset;
auto result = std::search(offset, fBuffer.cend(), kNewLine.cbegin(), kNewLine.cend());
if (result == fBuffer.cend())
return std::nullopt;
BString line(reinterpret_cast<const char*>(std::addressof(*offset)), std::distance(offset, result));
fCurrentOffset = std::distance(fBuffer.cbegin(), result) + 2;
return line;
}
/*!
\brief Get the number of remaining bytes in this buffer.
*/
size_t
HttpBuffer::RemainingBytes() noexcept
{
return fBuffer.size() - fCurrentOffset;
}
/*!
\brief Move data to the beginning of the buffer to clear at the back.
The GetNextLine() increases the offset of the internal buffer. This call moves remaining data
to the beginning of the buffer sets the correct size, making the remainder of the capacity
available for further reading.
*/
void
HttpBuffer::Flush() noexcept
{
if (fCurrentOffset > 0) {
auto end = fBuffer.cbegin() + fCurrentOffset;
fBuffer.erase(fBuffer.cbegin(), end);
fCurrentOffset = 0;
}
}
/*!
\brief Clear the internal buffer
*/
void
HttpBuffer::Clear() noexcept
{
fBuffer.clear();
fCurrentOffset = 0;
}
/*!
\brief Parse the status from the \a buffer and store it in \a status.
\retval true The status was succesfully parsed
\retval false There is not enough data in the buffer for a full status.
\exception BNetworkRequestException The status does not conform to the HTTP spec.
*/
bool
HttpParser::ParseStatus(HttpBuffer& buffer, BHttpStatus& status)
{
auto statusLine = buffer.GetNextLine();
if (!statusLine)
return false;
auto codeStart = statusLine->FindFirst(' ') + 1;
if (codeStart < 0)
throw BNetworkRequestError(__PRETTY_FUNCTION__, BNetworkRequestError::ProtocolError);
auto codeEnd = statusLine->FindFirst(' ', codeStart);
if (codeEnd < 0 || (codeEnd - codeStart) != 3)
throw BNetworkRequestError(__PRETTY_FUNCTION__, BNetworkRequestError::ProtocolError);
std::string statusCodeString(statusLine->String() + codeStart, 3);
// build the output
try {
status.code = std::stol(statusCodeString);
} catch (...) {
throw BNetworkRequestError(__PRETTY_FUNCTION__, BNetworkRequestError::ProtocolError);
}
status.text = std::move(statusLine.value());
return true;
}
/*!
\brief Parse the fields from the \a buffer and store it in \a fields.
The fields are parsed incrementally, meaning that even if the full header is not yet in the
\a buffer, it will still parse all complete fields and store them in the \a fields.
\retval true All fields were succesfully parsed
\retval false There is not enough data in the buffer to complete parsing of fields.
\exception BNetworkRequestException The fields not conform to the HTTP spec.
*/
bool
HttpParser::ParseFields(HttpBuffer& buffer, BHttpFields& fields)
{
auto fieldLine = buffer.GetNextLine();
while (fieldLine && !fieldLine.value().IsEmpty()){
// Parse next header line
fields.AddField(fieldLine.value());
fieldLine = buffer.GetNextLine();
}
if (fieldLine && fieldLine.value().IsEmpty()){
// end of the header section of the message
return true;
} else {
// there is more to parse
return false;
}
}
/*!
\brief Parse the Gzip compression from the body.
\exception std::bad_alloc in case there is an error allocating memory.
*/
void
HttpParser::SetGzipCompression(bool compression)
{
if (compression) {
fDecompressorStorage = std::make_unique<BMallocIO>();
BDataIO* stream = nullptr;
auto result = BZlibCompressionAlgorithm()
.CreateDecompressingOutputStream(fDecompressorStorage.get(), nullptr, stream);
if (result != B_OK) {
throw BNetworkRequestError(
"BZlibCompressionAlgorithm().CreateCompressingOutputStream",
BNetworkRequestError::SystemError, result);
}
fDecompressingStream = std::unique_ptr<BDataIO>(stream);
} else {
fDecompressingStream = nullptr;
fDecompressorStorage = nullptr;
}
}
/*!
\brief Set the content length of the body.
If a content length is set, the body will not be handled as a chunked transfer.
*/
void
HttpParser::SetContentLength(std::optional<off_t> contentLength) noexcept
{
fBodyBytesTotal = contentLength;
}
/*!
\brief Parse the body from the \a buffer and use \a writeToBody function to save.
*/
size_t
HttpParser::ParseBody(HttpBuffer& buffer, HttpTransferFunction writeToBody)
{
if (fBodyBytesTotal.has_value()) {
return _ParseBodyRaw(buffer, writeToBody);
} else {
return _ParseBodyChunked(buffer, writeToBody);
}
}
/*!
\brief Parse the body from the \a buffer and use \a writeToBody function to save.
*/
size_t
HttpParser::_ParseBodyRaw(HttpBuffer& buffer, HttpTransferFunction writeToBody)
{
if (fBodyBytesTotal && (fTransferredBodySize + static_cast<off_t>(buffer.RemainingBytes()))
> *fBodyBytesTotal)
throw BNetworkRequestError(__PRETTY_FUNCTION__, BNetworkRequestError::ProtocolError);
auto bytesToRead = buffer.RemainingBytes();
auto readEnd = fBodyBytesTotal.value()
== (fTransferredBodySize + static_cast<off_t>(bytesToRead));
auto bytesRead = _ReadChunk(buffer, writeToBody, bytesToRead, readEnd);
fTransferredBodySize += bytesRead;
return bytesRead;
}
/*!
\brief Parse the body from the \a buffer and use \a writeToBody function to save.
*/
size_t
HttpParser::_ParseBodyChunked(HttpBuffer& buffer, HttpTransferFunction writeToBody)
{
size_t totalBytesRead = 0;
while (buffer.RemainingBytes() > 0) {
switch (fBodyState) {
case HttpBodyInputStreamState::ChunkSize:
{
// Read the next chunk size from the buffer; if unsuccesful wait for more data
auto chunkSizeString = buffer.GetNextLine();
if (!chunkSizeString)
return totalBytesRead;
auto chunkSizeStr = std::string(chunkSizeString.value().String());
try {
size_t pos = 0;
fRemainingChunkSize = std::stoll(chunkSizeStr, &pos, 16);
if (pos < chunkSizeStr.size() && chunkSizeStr[pos] != ';'){
throw BNetworkRequestError(__PRETTY_FUNCTION__,
BNetworkRequestError::ProtocolError);
}
} catch (const std::invalid_argument&) {
throw BNetworkRequestError(__PRETTY_FUNCTION__,
BNetworkRequestError::ProtocolError);
} catch (const std::out_of_range&) {
throw BNetworkRequestError(__PRETTY_FUNCTION__,
BNetworkRequestError::ProtocolError);
}
if (fRemainingChunkSize > 0)
fBodyState = HttpBodyInputStreamState::Chunk;
else
fBodyState = HttpBodyInputStreamState::Trailers;
break;
}
case HttpBodyInputStreamState::Chunk:
{
size_t bytesToRead;
bool readEnd = false;
if (fRemainingChunkSize > static_cast<off_t>(buffer.RemainingBytes()))
bytesToRead = buffer.RemainingBytes();
else {
readEnd = true;
bytesToRead = fRemainingChunkSize;
}
auto bytesRead = _ReadChunk(buffer, writeToBody, bytesToRead, readEnd);
fTransferredBodySize += bytesRead;
totalBytesRead += bytesRead;
fRemainingChunkSize -= bytesRead;
if (fRemainingChunkSize == 0)
fBodyState = HttpBodyInputStreamState::ChunkEnd;
break;
}
case HttpBodyInputStreamState::ChunkEnd:
{
if (buffer.RemainingBytes() < 2) {
// not enough data in the buffer to finish the chunk
return totalBytesRead;
}
auto chunkEndString = buffer.GetNextLine();
if (!chunkEndString || chunkEndString.value().Length() != 0) {
// There should have been an empty chunk
throw BNetworkRequestError(__PRETTY_FUNCTION__,
BNetworkRequestError::ProtocolError);
}
fBodyState = HttpBodyInputStreamState::ChunkSize;
break;
}
case HttpBodyInputStreamState::Trailers:
{
auto trailerString = buffer.GetNextLine();
if (!trailerString) {
// More data to come
return totalBytesRead;
}
if (trailerString.value().Length() > 0) {
// Ignore empty trailers for now
// TODO: review if the API should support trailing headers
} else {
fBodyState = HttpBodyInputStreamState::Done;
return totalBytesRead;
}
break;
}
case HttpBodyInputStreamState::Done:
return totalBytesRead;
}
}
return totalBytesRead;
}
/*!
\brief Check if the body is fully parsed.
*/
bool
HttpParser::Complete() const noexcept
{
if (_IsChunked())
return fBodyState == HttpBodyInputStreamState::Done;
return fBodyBytesTotal.value() == fTransferredBodySize;
}
/*!
\brief Read a chunk of data from the buffer and write it to the output.
If there is a compression algorithm applied, then it passes through the compression first.
\param buffer The buffer to read from
\param writeToBody The function that can write data from the buffer to the body.
\param size The maximum size to read from the buffer. When larger than the buffer size, the
remaining bytes from the buffer are read.
\param flush Setting this parameter will force the decompression to write out all data, if
applicable. Set when all the data has been received.
\exception BNetworkRequestError When there was any error with any of the system cals.
*/
size_t
HttpParser::_ReadChunk(HttpBuffer& buffer, HttpTransferFunction writeToBody, size_t size, bool flush)
{
if (size == 0)
return 0;
if (size > buffer.RemainingBytes())
size = buffer.RemainingBytes();
if (fDecompressingStream) {
buffer.WriteTo([this](const std::byte* buffer, size_t bufferSize){
auto status = fDecompressingStream->WriteExactly(buffer, bufferSize);
if (status != B_OK) {
throw BNetworkRequestError("BDataIO::WriteExactly()",
BNetworkRequestError::SystemError, status);
}
return bufferSize;
}, size);
if (flush) {
// No more bytes expected so flush out the final bytes
if (auto status = fDecompressingStream->Flush(); status != B_OK)
throw BNetworkRequestError("BZlibDecompressionStream::Flush()",
BNetworkRequestError::SystemError, status);
}
if (auto bodySize = fDecompressorStorage->Position(); bodySize > 0) {
auto bytesWritten
= writeToBody(static_cast<const std::byte*>(fDecompressorStorage->Buffer()),
bodySize);
if (static_cast<off_t>(bytesWritten) != bodySize) {
throw BNetworkRequestError(__PRETTY_FUNCTION__,
BNetworkRequestError::SystemError, B_PARTIAL_WRITE);
}
fDecompressorStorage->Seek(0, SEEK_SET);
}
} else {
// Write the body directly to the target
buffer.WriteTo(writeToBody, size);
}
return size;
}
/*!
\brief Internal helper to determine if the body is sent as a chunked transfer.
*/
bool
HttpParser::_IsChunked() const noexcept
{
return fBodyBytesTotal == std::nullopt;
}
+2
View File
@@ -19,7 +19,9 @@ for architectureObject in [ MultiArchSubDirSetup ] {
StaticLibrary [ MultiArchDefaultGristFiles libnetservices2.a ] :
ErrorsExt.cpp
HttpBuffer.cpp
HttpFields.cpp
HttpParser.cpp
HttpRequest.cpp
HttpResult.cpp
HttpSession.cpp