blob: c76662e20a733e77475095d518b8789f7e2f52e2 [file]
/*
* 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 <boost/test/unit_test.hpp>
#include <algorithm>
#include <cstring>
#include <locale>
#include <memory>
#include <sstream>
#include <string>
#include <vector>
#include <thrift/TConfiguration.h>
#include <thrift/transport/TBufferTransports.h>
#include <thrift/transport/THttpClient.h>
#include <thrift/transport/THttpServer.h>
#include <thrift/transport/TTransportException.h>
#include <thrift/transport/TVirtualTransport.h>
/*
* 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<apache::thrift::transport::TTransport> transport,
std::shared_ptr<TConfiguration> config)
: THttpServer(transport, config) {}
uint32_t bufferSize() const { return httpBufSize_; }
};
class TestHttpClient : public THttpClient {
public:
TestHttpClient(std::shared_ptr<apache::thrift::transport::TTransport> transport,
std::string host,
std::string path,
std::shared_ptr<TConfiguration> 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<CountingTransport> {
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<uint32_t>((std::min)(static_cast<size_t>(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<TestHttpServer> makeServer(const std::string& wire,
std::shared_ptr<std::string>* 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<std::string>(wire);
auto inner = std::make_shared<TMemoryBuffer>(
reinterpret_cast<uint8_t*>(const_cast<char*>((*keepAlive)->data())),
static_cast<uint32_t>((*keepAlive)->size()));
auto config = std::make_shared<TConfiguration>(static_cast<int>(kMaxMessageSize));
return std::make_shared<TestHttpServer>(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<std::string> 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<std::string> 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<char*>(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<std::string> 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<char*>(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<CountingTransport>(wire);
auto config = std::make_shared<TConfiguration>(static_cast<int>(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<CountingTransport>(wire);
auto config = std::make_shared<TConfiguration>(static_cast<int>(kMaxMessageSize));
TestHttpServer trans(inner, config);
std::vector<uint8_t> out(body.size());
BOOST_CHECK_EQUAL(trans.readAll(out.data(), static_cast<uint32_t>(out.size())), out.size());
BOOST_CHECK_EQUAL(std::string(reinterpret_cast<char*>(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<CountingTransport>(wire);
auto config = std::make_shared<TConfiguration>(static_cast<int>(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<uint8_t> out(4096);
bool threw = false;
try {
for (uint32_t taken = 0; taken < 4 * kMaxMessageSize; taken += static_cast<uint32_t>(out.size())) {
client.readAll(out.data(), static_cast<uint32_t>(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<CountingTransport>(one + one);
auto config = std::make_shared<TConfiguration>(static_cast<int>(kMaxMessageSize));
TestHttpServer trans(inner, config);
std::vector<uint8_t> out(body.size());
BOOST_CHECK_EQUAL(trans.readAll(out.data(), static_cast<uint32_t>(out.size())), out.size());
trans.flush();
BOOST_CHECK_EQUAL(trans.readAll(out.data(), static_cast<uint32_t>(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<std::string>(wire);
auto inner = std::make_shared<TMemoryBuffer>(
reinterpret_cast<uint8_t*>(const_cast<char*>(keepAlive->data())),
static_cast<uint32_t>(keepAlive->size()));
auto config = std::make_shared<TConfiguration>(static_cast<int>(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<char*>(out), 5), "hello");
}
BOOST_AUTO_TEST_SUITE_END()