diff --git a/src/ChunkedEncoding.h b/src/ChunkedEncoding.h index c96bbdc8d..397a1c12f 100644 --- a/src/ChunkedEncoding.h +++ b/src/ChunkedEncoding.h @@ -203,6 +203,7 @@ namespace uWS { if (chunkSize(state) == 0) { // Start of line: \r means final empty line, else it's a trailer header if (c == '\r') state = (state & ~STATE_SIZE_MASK) | 1; + else if (c == '\n') { state = STATE_IS_ERROR; return std::nullopt; } else state = (state & ~STATE_SIZE_MASK) | 2; } else if (chunkSize(state) == 1) { // Expecting \n of final empty line @@ -215,6 +216,7 @@ namespace uWS { } else if (chunkSize(state) == 2) { // Inside trailer header, wait for \r if (c == '\r') state = (state & ~STATE_SIZE_MASK) | 3; + else if (c == '\n') { state = STATE_IS_ERROR; return std::nullopt; } } else if (chunkSize(state) == 3) { // Expecting \n to terminate the header line if (c == '\n') state = (state & ~STATE_SIZE_MASK) | 0; diff --git a/src/HttpParser.h b/src/HttpParser.h index 4da770438..e0cbe38e8 100644 --- a/src/HttpParser.h +++ b/src/HttpParser.h @@ -610,7 +610,7 @@ struct HttpParser { if (!CONSUME_MINIMALLY) { /* Go ahead and parse it (todo: better heuristics for emitting FIN to the app level) */ std::string_view dataToConsume(data, length); - for (auto chunk : uWS::ChunkIterator(&dataToConsume, &remainingStreamingBytes)) { + for (auto chunk : uWS::ChunkIterator(&dataToConsume, &remainingStreamingBytes, true)) { dataHandler(user, chunk, chunk.length() ? UINT64_MAX : 0); } if (isParsingInvalidChunkedEncoding(remainingStreamingBytes)) { @@ -672,7 +672,7 @@ struct HttpParser { /* It's either chunked or with a content-length */ if (isParsingChunkedEncoding(remainingStreamingBytes)) { std::string_view dataToConsume(data, length); - for (auto chunk : uWS::ChunkIterator(&dataToConsume, &remainingStreamingBytes)) { + for (auto chunk : uWS::ChunkIterator(&dataToConsume, &remainingStreamingBytes, true)) { /* If we got the zero size chunk, maxRemainingBodyLength is 0, else it is practically infinity */ dataHandler(user, chunk, chunk.length() ? UINT64_MAX : 0); } @@ -730,7 +730,7 @@ struct HttpParser { /* It's either chunked or with a content-length */ if (isParsingChunkedEncoding(remainingStreamingBytes)) { std::string_view dataToConsume(data, length); - for (auto chunk : uWS::ChunkIterator(&dataToConsume, &remainingStreamingBytes)) { + for (auto chunk : uWS::ChunkIterator(&dataToConsume, &remainingStreamingBytes, true)) { dataHandler(user, chunk, chunk.length() ? UINT64_MAX : 0); } if (isParsingInvalidChunkedEncoding(remainingStreamingBytes)) { diff --git a/tests/HttpParser.cpp b/tests/HttpParser.cpp index b924471e5..a0de988bf 100644 --- a/tests/HttpParser.cpp +++ b/tests/HttpParser.cpp @@ -36,6 +36,41 @@ int main() { std::cout << "HTTP DONE" << std::endl; + /* RFC 9112 7.1.2: trailer fields after the last chunk are skipped and the next request still parses. + * A bare LF there is a 400, else the next request would be read as a trailer field */ + struct { + const char *trailer; + bool accept; + } trailerCases[] = { + {"X-Checksum: abc\r\n\r\n", true}, + {"\n", false}, + {"X-Checksum: abc\n\r\n", false}, + }; + + for (auto &c : trailerCases) { + std::string req = std::string("POST / HTTP/1.1\r\nHost: 127.0.0.1\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n0\r\n") + c.trailer + + "GET / HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n"; + unsigned int length = (unsigned int) req.size(); + req.append(32, 'E'); + + int requests = 0; + void *trailerUser = (void *) 13; + uWS::HttpParser trailerParser; + auto [trailerErr, trailerReturned] = trailerParser.consumePostPadded(req.data(), length, trailerUser, nullptr, [&requests](void *s, uWS::HttpRequest *) -> void * { + requests++; + return s; + }, [](void *s, std::string_view, bool) -> void * { + return s; + }); + + bool ok = c.accept ? (trailerReturned == trailerUser && requests == 2) : trailerReturned != trailerUser; + if (!ok) { + std::cerr << "Trailer \"" << c.trailer << "\" expected " << (c.accept ? "accept" : "400") + << ", got err=" << trailerErr << " requests=" << requests << std::endl; + return 1; + } + } + /* Issue 1941: accept Transfer-Encoding when the final coding is chunked (case-insensitive). */ struct { const char *te;