blob: 0b24cff4e64caa20c3014dc0fbb5dd2ca9547fd2 [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 <memory>
#include <string>
#include <thrift/TConfiguration.h>
#include <thrift/transport/TBufferTransports.h>
#include <thrift/transport/THttpClient.h>
#include <thrift/transport/THttpServer.h>
#include <thrift/transport/TTransportException.h>
/*
* 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<TTransport> transport, std::shared_ptr<TConfiguration> 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<TTransport> transport,
std::string host,
std::string path,
std::shared_ptr<TConfiguration> config)
: THttpClient(transport, host, path, config) {}
uint32_t contentLength() const { return contentLength_; }
bool chunked() const { return chunked_; }
};
std::shared_ptr<TMemoryBuffer> wireBuffer(const std::string& wire,
std::shared_ptr<std::string>* keepAlive) {
*keepAlive = std::make_shared<std::string>(wire);
return std::make_shared<TMemoryBuffer>(
reinterpret_cast<uint8_t*>(const_cast<char*>((*keepAlive)->data())),
static_cast<uint32_t>((*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<std::string> keepAlive;
TestHttpServer trans(wireBuffer(request("Content-length: 5"), &keepAlive),
std::make_shared<TConfiguration>());
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_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<std::string> keepAlive;
TestHttpServer trans(wireBuffer(request(header), &keepAlive),
std::make_shared<TConfiguration>());
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<std::string> keepAlive;
TestHttpServer trans(wireBuffer(request(header), &keepAlive),
std::make_shared<TConfiguration>());
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<std::string> keepAlive;
TestHttpServer trans(wireBuffer(request("X: 203.0.113.7"), &keepAlive),
std::make_shared<TConfiguration>());
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<std::string> keepAlive;
TestHttpClient client(wireBuffer(wire, &keepAlive), "localhost", "/",
std::make_shared<TConfiguration>());
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_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<std::string> keepAlive;
TestHttpServer trans(wireBuffer(request("Content-length: -1"), &keepAlive),
std::make_shared<TConfiguration>());
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<std::string> keepAlive;
TestHttpClient client(wireBuffer(wire, &keepAlive), "localhost", "/",
std::make_shared<TConfiguration>());
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<std::string> keepAlive;
TestHttpServer trans(wireBuffer(request(header), &keepAlive),
std::make_shared<TConfiguration>());
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<std::string> keepAlive;
TestHttpServer trans(wireBuffer(request("Content-length: 99999999999"), &keepAlive),
std::make_shared<TConfiguration>());
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()