Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 48 additions & 96 deletions src/web/http/Parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,46 +50,13 @@

#include "web/http/http_utils.h"

#include <charconv>
#include <system_error>
#include <tuple>
#include <utility>

#endif /* DOXYGEN_SHOULD_SKIP_THIS */

namespace web::http {

static bool parseContentLengthStrict(const std::string& s, std::size_t& out) {
bool success = false;

if (!s.empty()) {
unsigned long long value = 0;

const char* first = s.data();
const char* last = s.data() + s.size();

const auto [ptr, ec] = std::from_chars(first, last, value, 10);

if (ec == std::errc{} && ptr == last) {
out = static_cast<std::size_t>(value);
success = true;
}
}

return success;
}

static bool transferEncodingHasChunked(CiStringMap<std::string>& headers) {
bool hasChunked = false;

if (headers.contains("Transfer-Encoding")) {
const std::string& encoding = headers["Transfer-Encoding"];
hasChunked = web::http::ciContains(encoding, "chunked");
}

return hasChunked;
}

// HTTP/1.0 and HTTP/1.1
const std::regex Parser::httpVersionRegex("^HTTP/([1])[.]([0-1])$");

Expand Down Expand Up @@ -190,77 +157,57 @@ namespace web::http {
return consumed;
}

void Parser::analyzeHeader() {
bool success = true;

// Determine message framing.
// RFC 9112 §6.3: Transfer-Encoding (chunked) overrides Content-Length.
const bool hasChunked = transferEncodingHasChunked(headers);

if (hasChunked) {
transferEncoding = TransferEncoding::Chunked;
decoderQueue.emplace_back(new web::http::decoder::Chunked(socketContext));
void Parser::useChunkedBodyDecoder() {
transferEncoding = TransferEncoding::Chunked;
decoderQueue.emplace_back(new web::http::decoder::Chunked(socketContext));
configureTrailerDecoder();
}

if (headers.contains("Trailer")) {
std::string trailers = headers["Trailer"];
void Parser::useIdentityBodyDecoder(std::size_t length) {
contentLength = length;
transferEncoding = TransferEncoding::Identity;
decoderQueue.emplace_back(new web::http::decoder::Identity(socketContext, contentLength));
}

while (!trailers.empty()) {
std::string trailerField;
std::tie(trailerField, trailers) = httputils::str_split(trailers, ',');
httputils::str_trimm(trailerField);
trailerFieldsExpected.insert(trailerField);
trailerField.clear();
bool Parser::configureTrailerDecoder() {
if (headers.contains("Trailer")) {
for (std::string trailerField : httputils::splitCommaSeparatedTokens(headers["Trailer"])) {
if (!trailerField.empty()) {
trailerFieldsExpected.insert(std::move(trailerField));
}
trailerDecoder.setFieldsExpected(trailerFieldsExpected);
}
} else if (headers.contains("Content-Length")) {
std::size_t length = 0;
trailerDecoder.setFieldsExpected(trailerFieldsExpected);
}
return true;
}

bool Parser::allowsCloseDelimitedBody() const {
return true;
}

if (!parseContentLengthStrict(headers["Content-Length"], length)) {
void Parser::analyzeHeader() {
std::size_t length = 0;
switch (httputils::parseContentLength(headers, length)) {
case httputils::ContentLengthParseResult::Absent:
break;
case httputils::ContentLengthParseResult::Valid:
useIdentityBodyDecoder(length);
break;
case httputils::ContentLengthParseResult::Invalid:
parseError(400, "Invalid Content-Length");
success = false;
} else {
contentLength = length;
decoderQueue.emplace_back(new web::http::decoder::Identity(socketContext, contentLength));
}
return;
}

if (success) {
// Transfer-Encoding (other than chunked) is currently not implemented, but we keep the
// existing behavior of not altering the decoder queue here.
if (headers.contains("Transfer-Encoding")) {
const std::string& encoding = headers["Transfer-Encoding"];
if (web::http::ciContains(encoding, "compressed")) {
// decoderQueue.emplace_back(new web::http::decoder::Compress(socketContext));
}
if (web::http::ciContains(encoding, "deflate")) {
// decoderQueue.emplace_back(new web::http::decoder::Deflate(socketContext));
}
if (web::http::ciContains(encoding, "gzip")) {
// decoderQueue.emplace_back(new web::http::decoder::GZip(socketContext));
}
}

if (decoderQueue.empty()) {
decoderQueue.emplace_back(new web::http::decoder::HTTP10Response(socketContext));
if (headers.contains("Transfer-Encoding") && httputils::headerHasToken(headers["Transfer-Encoding"], "chunked")) {
for (const ContentDecoder* contentDecoder : decoderQueue) {
delete contentDecoder;
}
decoderQueue.clear();
useChunkedBodyDecoder();
}

if (headers.contains("Content-Encoding")) {
const std::string& encoding = headers["Content-Encoding"];

if (web::http::ciContains(encoding, "compressed")) {
// decoderQueue.emplace_back(new web::http::decoder::Compress(socketContext));
}
if (web::http::ciContains(encoding, "deflate")) {
// decoderQueue.emplace_back(new web::http::decoder::Deflate(socketContext));
}
if (web::http::ciContains(encoding, "gzip")) {
// decoderQueue.emplace_back(new web::http::decoder::GZip(socketContext));
}
if (web::http::ciContains(encoding, "br")) {
// decoderQueue.emplace_back(new web::http::decoder::Br(socketContext));
}
}
if (decoderQueue.empty() && allowsCloseDelimitedBody()) {
decoderQueue.emplace_back(new web::http::decoder::HTTP10Response(socketContext));
}
}

Expand All @@ -275,7 +222,7 @@ namespace web::http {
std::vector<char> chunk = contentDecoder->getContent();
content.insert(content.end(), chunk.begin(), chunk.end());

if (transferEncoding == TransferEncoding::Chunked && headers.contains("Trailer")) {
if (transferEncoding == TransferEncoding::Chunked) {
parserState = Parser::ParserState::TRAILER;
} else {
parsingFinished();
Expand All @@ -294,7 +241,12 @@ namespace web::http {
parseError(trailerDecoder.getErrorCode(), trailerDecoder.getErrorReason());
} else if (trailerDecoder.isComplete()) {
web::http::CiStringMap<std::string>&& trailer = trailerDecoder.getHeader();
headers.insert(trailer.begin(), trailer.end());
for (const auto& [field, value] : trailer) {
if (!web::http::ciEquals(field, "Content-Length") && !web::http::ciEquals(field, "Transfer-Encoding") &&
!web::http::ciEquals(field, "Host") && !web::http::ciEquals(field, "Connection")) {
headers.insert({field, value});
}
}
parsingFinished();
}

Expand Down
4 changes: 4 additions & 0 deletions src/web/http/Parser.h
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ namespace web::http {

protected:
virtual void analyzeHeader();
virtual bool allowsCloseDelimitedBody() const;
void useChunkedBodyDecoder();
void useIdentityBodyDecoder(std::size_t length);
bool configureTrailerDecoder();

private:
virtual void parseError(int code, const std::string& reason) = 0;
Expand Down
6 changes: 3 additions & 3 deletions src/web/http/client/ResponseParser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,10 @@ namespace web::http::client {

if (headers.contains("Connection")) {
const std::string& connection = headers["Connection"];
if (web::http::ciContains(connection, "keep-alive")) {
response.connectionState = ConnectionState::Keep;
} else if (web::http::ciContains(connection, "close")) {
if (httputils::headerHasToken(connection, "close")) {
response.connectionState = ConnectionState::Close;
} else if (httputils::headerHasToken(connection, "keep-alive")) {
response.connectionState = ConnectionState::Keep;
}
}
if (headers.contains("Set-Cookie")) {
Expand Down
51 changes: 43 additions & 8 deletions src/web/http/decoder/Chunked.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,12 @@
#include "web/http/decoder/Chunked.h"

#include "core/socket/stream/SocketContext.h"
#include "web/http/http_utils.h"

#include <algorithm>
#include <cctype>
#include <limits>
#include <tuple>
#include <stdexcept>

#ifndef DOXYGEN_SHOULD_SKIP_THIS
Expand Down Expand Up @@ -83,7 +88,7 @@ namespace web::http::decoder {
}

state = (completed || error) ? -1 : state;
} while (ret > 0 && !completed);
} while (ret > 0 && !completed && !error);
break;
}

Expand Down Expand Up @@ -149,14 +154,44 @@ namespace web::http::decoder {
CR = false;
LF = false;

try {
chunkLenTotal = std::stoul(chunkLenTotalS, &pos, 16);
chunk.resize(chunkLenTotal);
{
std::string chunkSizeToken = chunkLenTotalS;
std::tie(chunkSizeToken, std::ignore) = httputils::str_split(chunkSizeToken, ';');
httputils::str_trimm(chunkSizeToken);
if (chunkSizeToken.empty() || !std::all_of(chunkSizeToken.begin(), chunkSizeToken.end(), [](unsigned char c) {
return std::isxdigit(c) != 0;
}) ||
chunkSizeToken.size() > maxChunkLenTotalS) {
error = true;
state = -1;
break;
}

state = 1;
} catch (std::invalid_argument&) {
error = true;
break;
try {
const unsigned long long parsedChunkLen = std::stoull(chunkSizeToken, &pos, 16);
if (pos != chunkSizeToken.size() || parsedChunkLen > std::numeric_limits<std::size_t>::max()) {
error = true;
state = -1;
break;
}
chunkLenTotal = static_cast<std::size_t>(parsedChunkLen);
chunk.resize(chunkLenTotal);

if (chunkLenTotal == 0) {
completed = true;
state = -1;
break;
}
state = 1;
} catch (std::invalid_argument&) {
error = true;
state = -1;
break;
} catch (std::out_of_range&) {
error = true;
state = -1;
break;
}
}

[[fallthrough]];
Expand Down
17 changes: 12 additions & 5 deletions src/web/http/decoder/Fields.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -129,17 +129,24 @@ namespace web::http::decoder {
}

void Fields::splitLine(const std::string& line) {
auto [headerFieldName, value] = httputils::str_split(line, ':');
const std::size_t colon = line.find(':');

if (colon == std::string::npos) {
errorCode = 400;
errorReason = "Header field missing colon";
return;
}

std::string headerFieldName = line.substr(0, colon);
std::string value = line.substr(colon + 1);

if (headerFieldName.empty()) {
errorCode = 400;
errorReason = "Header field empty";
} else if ((std::isblank(headerFieldName.back()) != 0) || (std::isblank(headerFieldName.front()) != 0)) {
} else if ((std::isblank(static_cast<unsigned char>(headerFieldName.back())) != 0) ||
(std::isblank(static_cast<unsigned char>(headerFieldName.front())) != 0)) {
errorCode = 400;
errorReason = "White space before or after field";
} else if (value.empty()) {
errorCode = 400;
errorReason = "Value of field \"" + headerFieldName + "\" empty";
} else {
if (fieldsExpected.empty() || fieldsExpected.contains(headerFieldName)) {
httputils::str_trimm(value);
Expand Down
Loading
Loading