/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ #include #include #include #include #include #include #include #include /* * THttpServer::parseHeader compared a header name for as many characters as * the *received* name happened to have: * * size_t sz = colon - header; * if (THRIFT_strncasecmp(header, "Content-length", sz) == 0) ... * * so every name that is a prefix of a name the transport knows was accepted * as that name -- "C: 5" set the content length and "T: chunked" switched on * chunked decoding. A header name means what RFC 9110 5.1 says it means, and * a transport that reads it otherwise ends up disagreeing with every other * party on the connection about where the message ends. * * The value side is the same question about a number: atoi() cannot report * that it was handed a negative value or something that is not a number at * all, and "Content-length: -1" therefore arrived in the uint32_t member as * 4294967295. */ BOOST_AUTO_TEST_SUITE(THttpHeaderParseTest) using apache::thrift::TConfiguration; using apache::thrift::transport::THttpClient; using apache::thrift::transport::THttpServer; using apache::thrift::transport::TMemoryBuffer; using apache::thrift::transport::TTransport; using apache::thrift::transport::TTransportException; namespace { // The parsed framing is what these tests are about, so they read it directly // rather than inferring it from how many bytes came back. class TestHttpServer : public THttpServer { public: TestHttpServer(std::shared_ptr transport, std::shared_ptr config) : THttpServer(transport, config) {} uint32_t contentLength() const { return contentLength_; } bool chunked() const { return chunked_; } const std::string& origin() const { return origin_; } }; class TestHttpClient : public THttpClient { public: TestHttpClient(std::shared_ptr transport, std::string host, std::string path, std::shared_ptr config) : THttpClient(transport, host, path, config) {} uint32_t contentLength() const { return contentLength_; } bool chunked() const { return chunked_; } }; std::shared_ptr wireBuffer(const std::string& wire, std::shared_ptr* keepAlive) { *keepAlive = std::make_shared(wire); return std::make_shared( reinterpret_cast(const_cast((*keepAlive)->data())), static_cast((*keepAlive)->size())); } // A request whose only interesting part is the one header under test, with a // five byte body behind it so that a transport which believes the header can // go and fetch one. std::string request(const std::string& header) { return "POST / HTTP/1.1\r\nHost: localhost\r\n" + header + "\r\n\r\nhello"; } } // namespace BOOST_AUTO_TEST_CASE(a_full_content_length_header_is_read) { std::shared_ptr keepAlive; TestHttpServer trans(wireBuffer(request("Content-length: 5"), &keepAlive), std::make_shared()); uint8_t out[5]; BOOST_CHECK_EQUAL(trans.read(out, sizeof(out)), 5u); BOOST_CHECK_EQUAL(std::string(reinterpret_cast(out), 5), "hello"); BOOST_CHECK_EQUAL(trans.contentLength(), 5u); } BOOST_AUTO_TEST_CASE(the_header_name_is_matched_in_full_and_not_by_prefix) { // "C" is not a header name this transport knows, and no other party on the // connection would read it as one either. const char* const prefixes[] = {"C: 5", "Co: 5", "Content: 5", "Content-len: 5"}; for (const char* header : prefixes) { std::shared_ptr keepAlive; TestHttpServer trans(wireBuffer(request(header), &keepAlive), std::make_shared()); uint8_t out[5]; // No content length, so there is no body to hand out. trans.read(out, sizeof(out)); BOOST_CHECK_MESSAGE(trans.contentLength() == 0u, std::string("\"") + header + "\" was read as a content length"); } } BOOST_AUTO_TEST_CASE(a_prefix_of_transfer_encoding_does_not_switch_on_chunking) { const char* const prefixes[] = {"T: chunked", "Tr: chunked", "Transfer: chunked"}; for (const char* header : prefixes) { std::shared_ptr keepAlive; TestHttpServer trans(wireBuffer(request(header), &keepAlive), std::make_shared()); uint8_t out[5]; // Reading a body that is not chunked as though it were runs the stream // out, so both outcomes end in an exception here and only the parsed flag // tells them apart. try { trans.read(out, sizeof(out)); } catch (const TTransportException&) { } BOOST_CHECK_MESSAGE(!trans.chunked(), std::string("\"") + header + "\" switched on chunked decoding"); } } BOOST_AUTO_TEST_CASE(a_prefix_of_x_forwarded_for_does_not_set_the_origin) { std::shared_ptr keepAlive; TestHttpServer trans(wireBuffer(request("X: 203.0.113.7"), &keepAlive), std::make_shared()); uint8_t out[5]; trans.read(out, sizeof(out)); BOOST_CHECK(trans.origin().empty()); } BOOST_AUTO_TEST_CASE(the_full_transfer_encoding_header_still_works) { std::string wire = "HTTP/1.1 200 OK\r\n" "Transfer-Encoding: chunked\r\n" "\r\n" "3\r\nhel\r\n" "2\r\nlo\r\n" "0\r\n\r\n"; std::shared_ptr keepAlive; TestHttpClient client(wireBuffer(wire, &keepAlive), "localhost", "/", std::make_shared()); uint8_t out[5]; BOOST_CHECK_EQUAL(client.readAll(out, sizeof(out)), 5u); BOOST_CHECK_EQUAL(std::string(reinterpret_cast(out), 5), "hello"); BOOST_CHECK(client.chunked()); } BOOST_AUTO_TEST_CASE(a_negative_content_length_is_refused_by_the_server) { // atoi() gave -1, which the uint32_t member then held as 4294967295. std::shared_ptr keepAlive; TestHttpServer trans(wireBuffer(request("Content-length: -1"), &keepAlive), std::make_shared()); uint8_t out[5]; BOOST_CHECK_THROW(trans.read(out, sizeof(out)), TTransportException); // Not "did it throw" -- an over-declared length outruns a five byte body and // throws END_OF_FILE on the unmodified library too. What is under test is // that no length was taken from "-1" at all. BOOST_CHECK_EQUAL(trans.contentLength(), 0u); } BOOST_AUTO_TEST_CASE(a_negative_content_length_is_refused_by_the_client) { std::string wire = "HTTP/1.1 200 OK\r\nContent-Length: -1\r\n\r\nhello"; std::shared_ptr keepAlive; TestHttpClient client(wireBuffer(wire, &keepAlive), "localhost", "/", std::make_shared()); uint8_t out[5]; BOOST_CHECK_THROW(client.readAll(out, sizeof(out)), TTransportException); BOOST_CHECK_EQUAL(client.contentLength(), 0u); } BOOST_AUTO_TEST_CASE(a_content_length_that_is_not_a_number_is_refused) { const char* const values[] = {"Content-length: abc", "Content-length: ", "Content-length: 5x"}; for (const char* header : values) { std::shared_ptr keepAlive; TestHttpServer trans(wireBuffer(request(header), &keepAlive), std::make_shared()); uint8_t out[5]; BOOST_CHECK_THROW(trans.read(out, sizeof(out)), TTransportException); BOOST_CHECK_EQUAL(trans.contentLength(), 0u); } } BOOST_AUTO_TEST_CASE(a_content_length_beyond_uint32_is_refused) { // atoi() has no way to say "that does not fit"; it is undefined behaviour, // and in practice the value arrived truncated. std::shared_ptr keepAlive; TestHttpServer trans(wireBuffer(request("Content-length: 99999999999"), &keepAlive), std::make_shared()); uint8_t out[5]; BOOST_CHECK_THROW(trans.read(out, sizeof(out)), TTransportException); // Same trap as above: the unmodified library throws here as well, having // stored the truncated 1215752191 and then run out of body. The number is // the test. BOOST_CHECK_EQUAL(trans.contentLength(), 0u); } BOOST_AUTO_TEST_SUITE_END()