NetServices: move HttpBuffer and HttpParser into their own header/source
Change-Id: I5bc0d9df6f94c2cf1c39baa6206bf6f1db284705
This commit is contained in:
@@ -23,6 +23,7 @@ namespace Network {
|
|||||||
class BHttpFields;
|
class BHttpFields;
|
||||||
class BHttpRequest;
|
class BHttpRequest;
|
||||||
class BHttpStatus;
|
class BHttpStatus;
|
||||||
|
class HttpBuffer;
|
||||||
|
|
||||||
|
|
||||||
class BAbstractDataStream {
|
class BAbstractDataStream {
|
||||||
@@ -62,77 +63,6 @@ private:
|
|||||||
using HttpTransferFunction = std::function<size_t (const std::byte*, size_t)>;
|
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
|
} // 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 <Socket.h>
|
||||||
#include <ZlibCompressionAlgorithm.h>
|
#include <ZlibCompressionAlgorithm.h>
|
||||||
|
|
||||||
|
#include "HttpBuffer.h"
|
||||||
|
#include "HttpParser.h"
|
||||||
#include "HttpResultPrivate.h"
|
#include "HttpResultPrivate.h"
|
||||||
#include "NetServicesPrivate.h"
|
#include "NetServicesPrivate.h"
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,7 @@
|
|||||||
|
|
||||||
#include <HttpStream.h>
|
#include <HttpStream.h>
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <stdexcept>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include <DataIO.h>
|
#include <DataIO.h>
|
||||||
@@ -18,7 +16,8 @@
|
|||||||
#include <HttpRequest.h>
|
#include <HttpRequest.h>
|
||||||
#include <HttpResult.h>
|
#include <HttpResult.h>
|
||||||
#include <NetServicesDefs.h>
|
#include <NetServicesDefs.h>
|
||||||
#include <ZlibCompressionAlgorithm.h>
|
|
||||||
|
#include "HttpBuffer.h"
|
||||||
|
|
||||||
using namespace BPrivate::Network;
|
using namespace BPrivate::Network;
|
||||||
|
|
||||||
@@ -36,14 +35,6 @@ using namespace BPrivate::Network;
|
|||||||
static constexpr ssize_t kMaxBufferSize = 8192;
|
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
|
// #pragma mark -- ByteIOHelper base class
|
||||||
|
|
||||||
|
|
||||||
@@ -218,486 +209,3 @@ BHttpRequestStream::Transfer(BDataIO* target)
|
|||||||
auto complete = fRemainingHeaderSize == 0 && fTransferredBodySize == fTotalBodySize;
|
auto complete = fRemainingHeaderSize == 0 && fTransferredBodySize == fTotalBodySize;
|
||||||
return TransferInfo{bytesWritten, fTransferredBodySize, fTotalBodySize, complete};
|
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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ for architectureObject in [ MultiArchSubDirSetup ] {
|
|||||||
|
|
||||||
StaticLibrary [ MultiArchDefaultGristFiles libnetservices2.a ] :
|
StaticLibrary [ MultiArchDefaultGristFiles libnetservices2.a ] :
|
||||||
ErrorsExt.cpp
|
ErrorsExt.cpp
|
||||||
|
HttpBuffer.cpp
|
||||||
HttpFields.cpp
|
HttpFields.cpp
|
||||||
|
HttpParser.cpp
|
||||||
HttpRequest.cpp
|
HttpRequest.cpp
|
||||||
HttpResult.cpp
|
HttpResult.cpp
|
||||||
HttpSession.cpp
|
HttpSession.cpp
|
||||||
|
|||||||
Reference in New Issue
Block a user