/* * 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 #include #include #include #include #include #include /* * THttpTransport keeps the line it is reading in httpBuf_, and grows that * buffer by doubling httpBufSize_ whenever a line does not fit. Nothing * bounded the doubling, so a peer that never sends a CRLF decided how large * the buffer became: first by making the process hold it, and then, once * httpBufSize_ reached 2 GiB, by wrapping the uint32_t on the next doubling. * * These tests hold the growth to TConfiguration::maxMessageSize. The inner * transport is given a budget of its own, large enough that it is not what * ends the read -- otherwise the test would pass on the unmodified library * for the wrong reason. */ BOOST_AUTO_TEST_SUITE(THttpBufferBoundTest) using apache::thrift::TConfiguration; using apache::thrift::transport::THttpClient; using apache::thrift::transport::THttpServer; using apache::thrift::transport::TMemoryBuffer; using apache::thrift::transport::TTransportException; namespace { // Exposes the buffer size so a test can tell how far the doubling went. class TestHttpServer : public THttpServer { public: TestHttpServer(std::shared_ptr transport, std::shared_ptr config) : THttpServer(transport, config) {} uint32_t bufferSize() const { return httpBufSize_; } }; 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) {} }; // Counts what the peer was asked to hand over. A bound on a declared length // is only worth anything if it is applied before the bytes are read, and a // test that asks whether something threw cannot tell that apart from running // the stream out. class CountingTransport : public apache::thrift::transport::TVirtualTransport { public: explicit CountingTransport(std::string wire) : wire_(std::move(wire)), pos_(0), served_(0) {} uint32_t read(uint8_t* buf, uint32_t len) { if (pos_ >= wire_.size()) { return 0; } const uint32_t n = static_cast((std::min)(static_cast(len), wire_.size() - pos_)); std::memcpy(buf, wire_.data() + pos_, n); pos_ += n; served_ += n; return n; } void write(const uint8_t*, uint32_t) {} bool isOpen() const override { return true; } void open() override {} void close() override {} uint64_t served() const { return served_; } private: std::string wire_; size_t pos_; uint64_t served_; }; const uint32_t kMaxMessageSize = 64 * 1024; std::shared_ptr makeServer(const std::string& wire, std::shared_ptr* keepAlive) { // The inner transport keeps the library default, so the only limit that can // stop the growth below is the one THttpServer enforces on itself. *keepAlive = std::make_shared(wire); auto inner = std::make_shared( reinterpret_cast(const_cast((*keepAlive)->data())), static_cast((*keepAlive)->size())); auto config = std::make_shared(static_cast(kMaxMessageSize)); return std::make_shared(inner, config); } } // namespace BOOST_AUTO_TEST_CASE(header_line_without_crlf_is_bounded) { // A request line that never ends. Before the bound, the buffer doubled // until it had swallowed all of it. std::string wire = "POST / HTTP/1.1\r\nX-Filler: " + std::string(4 * 1024 * 1024, 'A'); std::shared_ptr keepAlive; auto trans = makeServer(wire, &keepAlive); uint8_t out[16]; BOOST_CHECK_THROW(trans->read(out, sizeof(out)), TTransportException); BOOST_CHECK_LE(trans->bufferSize(), kMaxMessageSize); } BOOST_AUTO_TEST_CASE(many_short_headers_are_bounded) { // Every line is short, so the buffer never has to grow, but the headers // together are far larger than the buffer. This has to keep working. std::string wire = "POST / HTTP/1.1\r\n"; for (int i = 0; i < 2000; ++i) { wire += "X-Filler-" + std::to_string(i) + ": " + std::string(60, 'A') + "\r\n"; } wire += "Content-Length: 5\r\n\r\nhello"; std::shared_ptr keepAlive; auto trans = makeServer(wire, &keepAlive); 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_LE(trans->bufferSize(), kMaxMessageSize); } BOOST_AUTO_TEST_CASE(a_long_but_bounded_header_line_still_works) { // One header line of 16 KB: longer than the initial buffer, well inside the // configured maximum, so the doubling still has to happen. std::string wire = "POST / HTTP/1.1\r\nX-Filler: " + std::string(16 * 1024, 'A') + "\r\nContent-Length: 5\r\n\r\nhello"; std::shared_ptr keepAlive; auto trans = makeServer(wire, &keepAlive); 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_GT(trans->bufferSize(), 1024u); BOOST_CHECK_LE(trans->bufferSize(), kMaxMessageSize); } BOOST_AUTO_TEST_CASE(a_declared_body_larger_than_the_maximum_is_refused) { // Four times the maximum is actually on the wire, so a transport that reads // the body before looking at the number will take all of it and only then // run out. The verdict is how much the peer was asked for. const std::string body(4 * kMaxMessageSize, 'A'); const std::string wire = "POST / HTTP/1.1\r\nContent-length: 1000000\r\n\r\n" + body; auto inner = std::make_shared(wire); auto config = std::make_shared(static_cast(kMaxMessageSize)); TestHttpServer trans(inner, config); uint8_t out[16]; BOOST_CHECK_THROW(trans.read(out, sizeof(out)), TTransportException); BOOST_CHECK_LT(inner->served(), kMaxMessageSize); } BOOST_AUTO_TEST_CASE(a_body_inside_the_maximum_is_read) { const std::string body(kMaxMessageSize / 2, 'A'); const std::string wire = "POST / HTTP/1.1\r\nContent-length: " + std::to_string(body.size()) + "\r\n\r\n" + body; auto inner = std::make_shared(wire); auto config = std::make_shared(static_cast(kMaxMessageSize)); TestHttpServer trans(inner, config); std::vector out(body.size()); BOOST_CHECK_EQUAL(trans.readAll(out.data(), static_cast(out.size())), out.size()); BOOST_CHECK_EQUAL(std::string(reinterpret_cast(out.data()), out.size()), body); } BOOST_AUTO_TEST_CASE(chunks_are_bounded_by_their_sum_and_not_one_at_a_time) { // Every chunk on its own is far inside the maximum; what the peer chooses is // how many of them there are. Without a running total this stream ends only // when the peer stops sending. const uint32_t chunkBytes = 4 * 1024; std::ostringstream size; // A chunk size is hex, and a stream follows whatever global locale is in // force, so a grouping locale turns 4096 into "1.000" and it stops being a // chunk size at all. THRIFT-6194 fixed the case that used to leave one set // for the rest of this binary; this stays as the local guarantee, which is // what TToString.h does for the same reason. size.imbue(std::locale::classic()); size << std::hex << chunkBytes; std::string wire = "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n"; for (uint32_t sent = 0; sent < 4 * kMaxMessageSize; sent += chunkBytes) { wire += size.str() + "\r\n" + std::string(chunkBytes, 'A') + "\r\n"; } wire += "0\r\n\r\n"; auto inner = std::make_shared(wire); auto config = std::make_shared(static_cast(kMaxMessageSize)); TestHttpClient client(inner, "localhost", "/", config); // Read in pieces well under the maximum: read() has always refused a single // request larger than it, and asking for the whole body at once would be // stopped by that check instead of by the one under test. std::vector out(4096); bool threw = false; try { for (uint32_t taken = 0; taken < 4 * kMaxMessageSize; taken += static_cast(out.size())) { client.readAll(out.data(), static_cast(out.size())); } } catch (const TTransportException&) { threw = true; } BOOST_CHECK(threw); // The chunk framing costs a few bytes per chunk on top of the payload, so // this is the maximum plus that overhead rather than the maximum exactly. BOOST_CHECK_LT(inner->served(), 2 * kMaxMessageSize); } BOOST_AUTO_TEST_CASE(a_second_request_gets_its_own_allowance) { // The maximum is per message. Two bodies that each fit have to both be read // on one connection, or a keep-alive connection would run out of allowance. const std::string body(kMaxMessageSize - 1024, 'A'); const std::string one = "POST / HTTP/1.1\r\nContent-length: " + std::to_string(body.size()) + "\r\n\r\n" + body; auto inner = std::make_shared(one + one); auto config = std::make_shared(static_cast(kMaxMessageSize)); TestHttpServer trans(inner, config); std::vector out(body.size()); BOOST_CHECK_EQUAL(trans.readAll(out.data(), static_cast(out.size())), out.size()); trans.flush(); BOOST_CHECK_EQUAL(trans.readAll(out.data(), static_cast(out.size())), out.size()); } BOOST_AUTO_TEST_CASE(a_chunked_response_still_works) { // Chunked bodies go through readLine() for every chunk size, so they share // the buffer that is now bounded. 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"; auto keepAlive = std::make_shared(wire); auto inner = std::make_shared( reinterpret_cast(const_cast(keepAlive->data())), static_cast(keepAlive->size())); auto config = std::make_shared(static_cast(kMaxMessageSize)); THttpClient client(inner, "localhost", "/", config); uint8_t out[5]; BOOST_CHECK_EQUAL(client.readAll(out, sizeof(out)), 5u); BOOST_CHECK_EQUAL(std::string(reinterpret_cast(out), 5), "hello"); } BOOST_AUTO_TEST_SUITE_END()