/* * 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. */ // Regression coverage for the standard TTransportFactory implementations: // getTransport() must hand the newly created wrapper the same TConfiguration // instance the wrapped endpoint already carries, rather than letting the // wrapper default-construct its own. doc/specs/thrift-tconfiguration.md // requires every layer of the transport/protocol stack to share one // TConfiguration instance by reference so that limits set by the caller // (e.g. maxFrameSize) stay in effect once a transport is wrapped by a // factory. // TBinaryWebSocketServerTransportFactory / TTextWebSocketServerTransportFactory // received the identical one-line fix (they share THttpServer's constructor // chain) but are intentionally not exercised here: TWebSocketServer.h pulls // in a hard, unconditional OpenSSL dependency (used for the RFC 6455 // handshake, unrelated to TConfiguration) that the rest of this file does // not otherwise need. #include #include #include #include #ifdef THRIFT_TEST_WITH_ZLIB #include #endif #include #include #define BOOST_TEST_MODULE TTransportFactoryConfigTest #include using namespace apache::thrift; using namespace apache::thrift::transport; namespace { std::shared_ptr endpointWithConfig(const std::shared_ptr& config) { return std::make_shared(config); } template void checkPreservesConfiguration(Factory& factory) { auto config = std::make_shared(); auto endpoint = endpointWithConfig(config); auto wrapped = factory.getTransport(endpoint); BOOST_REQUIRE(wrapped != nullptr); BOOST_CHECK_EQUAL(wrapped->getConfiguration().get(), config.get()); } } // namespace BOOST_AUTO_TEST_CASE(test_buffered_transport_factory_preserves_configuration) { TBufferedTransportFactory factory; checkPreservesConfiguration(factory); } BOOST_AUTO_TEST_CASE(test_framed_transport_factory_preserves_configuration) { TFramedTransportFactory factory; checkPreservesConfiguration(factory); } #ifdef THRIFT_TEST_WITH_ZLIB BOOST_AUTO_TEST_CASE(test_header_transport_factory_preserves_configuration) { THeaderTransportFactory factory; checkPreservesConfiguration(factory); } #endif // THRIFT_TEST_WITH_ZLIB BOOST_AUTO_TEST_CASE(test_http_server_transport_factory_preserves_configuration) { THttpServerTransportFactory factory; checkPreservesConfiguration(factory); } BOOST_AUTO_TEST_CASE(test_piped_transport_factory_preserves_configuration) { auto config = std::make_shared(); auto endpoint = endpointWithConfig(config); auto sink = std::make_shared(); TPipedTransportFactory factory(sink); auto wrapped = factory.getTransport(endpoint); BOOST_REQUIRE(wrapped != nullptr); BOOST_CHECK_EQUAL(wrapped->getConfiguration().get(), config.get()); } // Behavioral check mirroring the concrete impact of the configuration loss: // a transport wrapped by TFramedTransportFactory must enforce the caller's // maxFrameSize, not the library default (256 MB). The frame carries a real // (if dummy) 1024-byte payload -- with a fresh default configuration // (unfixed factory), a 1024-byte frame is well within the default limit and // is read successfully, so this only rejects for the intended reason // (CORRUPTED_DATA from the maxFrameSize check) rather than incidentally // failing on a truncated buffer. BOOST_AUTO_TEST_CASE(test_framed_transport_factory_enforces_custom_max_frame_size) { const int kMaxFrameSize = 64; auto config = std::make_shared(TConfiguration::DEFAULT_MAX_MESSAGE_SIZE, kMaxFrameSize); // A 4-byte big-endian frame-length prefix declaring a 1024-byte frame, // well beyond kMaxFrameSize, followed by 1024 bytes of payload. const uint32_t declaredFrameSize = 1024; std::vector frame = { static_cast((declaredFrameSize >> 24) & 0xFF), static_cast((declaredFrameSize >> 16) & 0xFF), static_cast((declaredFrameSize >> 8) & 0xFF), static_cast(declaredFrameSize & 0xFF), }; frame.resize(frame.size() + declaredFrameSize, 0x41); auto endpoint = std::make_shared(frame.data(), static_cast(frame.size()), TMemoryBuffer::COPY, config); TFramedTransportFactory factory; auto wrapped = factory.getTransport(endpoint); BOOST_CHECK_EQUAL(wrapped->getConfiguration().get(), config.get()); uint8_t dummy = 0; bool caught = false; try { wrapped->read(&dummy, 1); } catch (const TTransportException& e) { caught = true; BOOST_CHECK_EQUAL(e.getType(), TTransportException::CORRUPTED_DATA); } BOOST_CHECK(caught); } // THeaderTransport lives in libthriftz, which is only built with zlib. #ifdef THRIFT_TEST_WITH_ZLIB // Behavioral coverage for THeaderTransport: like TFramedTransport, the header // receive path must reject a frame larger than the caller-configured // maxFrameSize, not only the internal MAX_FRAME_SIZE ceiling (~1 GiB). The // frame below is a well-formed framed-binary frame (4-byte length prefix, a // TBinaryProtocol VERSION_1 magic word, then padding to the declared length); // an unfixed reader accepts it, so a rejection here is specifically the // configured maxFrameSize check and not an incidental parse failure. BOOST_AUTO_TEST_CASE(test_header_transport_enforces_custom_max_frame_size) { const int kMaxFrameSize = 64; auto config = std::make_shared(TConfiguration::DEFAULT_MAX_MESSAGE_SIZE, kMaxFrameSize); const uint32_t declaredFrameSize = 1024; // well beyond kMaxFrameSize const uint32_t version1 = 0x80010000u; // TBinaryProtocol::VERSION_1 std::vector frame = { static_cast((declaredFrameSize >> 24) & 0xFF), static_cast((declaredFrameSize >> 16) & 0xFF), static_cast((declaredFrameSize >> 8) & 0xFF), static_cast(declaredFrameSize & 0xFF), static_cast((version1 >> 24) & 0xFF), static_cast((version1 >> 16) & 0xFF), static_cast((version1 >> 8) & 0xFF), static_cast(version1 & 0xFF), }; // Pad the remainder of the declared frame body (magic already accounts for 4). frame.resize(frame.size() + declaredFrameSize - 4, 0x00); auto endpoint = std::make_shared(frame.data(), static_cast(frame.size()), TMemoryBuffer::COPY, config); auto transport = std::make_shared(endpoint, config); uint8_t dummy = 0; bool caught = false; try { transport->read(&dummy, 1); } catch (const TTransportException& e) { caught = true; BOOST_CHECK_EQUAL(e.getType(), TTransportException::CORRUPTED_DATA); } BOOST_CHECK(caught); } // Behavioral coverage for the post-transform path: a small ZLIB-compressed // header frame that inflates beyond the configured maxFrameSize must be // rejected, so a compressed frame cannot deliver a larger payload than an // uncompressed frame would be permitted to. The frame is produced by the real // header write path, so its framing and zlib stream are genuine. BOOST_AUTO_TEST_CASE(test_header_transport_enforces_max_frame_size_after_transform) { auto writeBuffer = std::make_shared(); THeaderTransport writer(writeBuffer); writer.setTransform(THeaderTransport::ZLIB_TRANSFORM); const std::string payload(600, 'B'); writer.write(reinterpret_cast(payload.data()), static_cast(payload.size())); writer.flush(); const std::string frame = writeBuffer->getBufferAsString(); // The compressed frame must be admitted by the size gate (so it is smaller // than kMaxFrameSize) while the 600-byte inflated payload exceeds it, leaving // the post-transform check as the only thing that can reject the frame. const int kMaxFrameSize = 256; BOOST_REQUIRE_LT(frame.size(), static_cast(kMaxFrameSize)); BOOST_REQUIRE_GT(payload.size(), static_cast(kMaxFrameSize)); auto config = std::make_shared(TConfiguration::DEFAULT_MAX_MESSAGE_SIZE, kMaxFrameSize); auto endpoint = std::make_shared(reinterpret_cast(const_cast(frame.data())), static_cast(frame.size()), TMemoryBuffer::COPY, config); auto reader = std::make_shared(endpoint, config); uint8_t dummy = 0; bool caught = false; try { reader->read(&dummy, 1); } catch (const TTransportException& e) { caught = true; BOOST_CHECK_EQUAL(e.getType(), TTransportException::CORRUPTED_DATA); } BOOST_CHECK(caught); } // Positive control: the same compressed frame read under a generous limit (the // library default maxFrameSize) must still round-trip. This guards against the // rejection tests above passing vacuously -- if maxFrameSize_ were left at 0 // for THeaderTransport, every frame would be rejected and those tests would // still pass. Here the default limit (~15.6 MiB) is far above the 600-byte // inflated payload, so a correct implementation must accept and deliver it. BOOST_AUTO_TEST_CASE(test_header_transport_accepts_frame_within_max_frame_size) { auto writeBuffer = std::make_shared(); THeaderTransport writer(writeBuffer); writer.setTransform(THeaderTransport::ZLIB_TRANSFORM); const std::string payload(600, 'B'); writer.write(reinterpret_cast(payload.data()), static_cast(payload.size())); writer.flush(); const std::string frame = writeBuffer->getBufferAsString(); // Default configuration -> default maxFrameSize; no custom limit is set. auto endpoint = std::make_shared(reinterpret_cast(const_cast(frame.data())), static_cast(frame.size()), TMemoryBuffer::COPY); auto reader = std::make_shared(endpoint); uint8_t first = 0; BOOST_CHECK_NO_THROW(reader->readAll(&first, 1)); BOOST_CHECK_EQUAL(first, static_cast('B')); } // A header frame lists a number of transforms and then the payload, and // untransform() applies each listed transform in turn. A frame that lists more // transforms than a legitimate writer ever emits (which is one) is refused when // the header is parsed, before the transform list is built and before any // transform runs. The frame is produced by the real header write path with the // transform set to three ZLIB layers, so its framing and zlib streams are // genuine; only the number of layers is what a correct reader must reject. BOOST_AUTO_TEST_CASE(test_header_transport_refuses_a_frame_with_more_than_one_transform) { auto writeBuffer = std::make_shared(); THeaderTransport writer(writeBuffer); writer.setTransform(THeaderTransport::ZLIB_TRANSFORM); writer.setTransform(THeaderTransport::ZLIB_TRANSFORM); writer.setTransform(THeaderTransport::ZLIB_TRANSFORM); const std::string payload(32, 'C'); writer.write(reinterpret_cast(payload.data()), static_cast(payload.size())); writer.flush(); const std::string frame = writeBuffer->getBufferAsString(); auto endpoint = std::make_shared(reinterpret_cast(const_cast(frame.data())), static_cast(frame.size()), TMemoryBuffer::COPY); auto reader = std::make_shared(endpoint); // The refusal is a transport-level CORRUPTED_DATA, the same class the other // frame checks in this transport raise. It is specifically not the protocol // exception a failed inflate would raise, which is how this asserts the frame // is refused before any transform is applied rather than during one. uint8_t dummy = 0; bool caught = false; try { reader->read(&dummy, 1); } catch (const TTransportException& e) { caught = true; BOOST_CHECK_EQUAL(e.getType(), TTransportException::CORRUPTED_DATA); } BOOST_CHECK(caught); } // Positive control: a frame that lists exactly one ZLIB transform -- the only // shape a legitimate writer produces -- must still round-trip unchanged. This // guards against the check above refusing valid traffic: the single-transform // frame is admitted, its one layer is inflated, and the payload is delivered // byte-for-byte. BOOST_AUTO_TEST_CASE(test_header_transport_single_transform_round_trips) { auto writeBuffer = std::make_shared(); THeaderTransport writer(writeBuffer); writer.setTransform(THeaderTransport::ZLIB_TRANSFORM); std::string payload(256, '\0'); for (size_t i = 0; i < payload.size(); i++) { payload[i] = static_cast(i & 0xff); } writer.write(reinterpret_cast(payload.data()), static_cast(payload.size())); writer.flush(); const std::string frame = writeBuffer->getBufferAsString(); auto endpoint = std::make_shared(reinterpret_cast(const_cast(frame.data())), static_cast(frame.size()), TMemoryBuffer::COPY); auto reader = std::make_shared(endpoint); std::vector out(payload.size(), 0); BOOST_CHECK_EQUAL(reader->readAll(out.data(), static_cast(out.size())), static_cast(out.size())); const std::string delivered(reinterpret_cast(out.data()), out.size()); BOOST_CHECK(delivered == payload); } #endif // THRIFT_TEST_WITH_ZLIB // TFramedTransport has three constructors. The two that take an underlying // transport initialise maxFrameSize_ from the configuration; the one that takes // only a TConfiguration leaves it out of its member-init list entirely, so // getMaxFrameSize() reads an uninitialised member and the frame-size limit it // reports is whatever happened to be on the stack. // // Reading it is undefined, so this is not a guaranteed detector: it fails // whenever the indeterminate value differs from the configured limit, which is // to say very nearly always, but a run that happens to find the right bytes // there would pass. What it does state unconditionally is the contract, which // is that all three constructors agree. BOOST_AUTO_TEST_CASE(test_framed_transport_default_constructor_initialises_max_frame_size) { TFramedTransport defaulted; BOOST_CHECK_EQUAL(defaulted.getMaxFrameSize(), static_cast(TConfiguration::DEFAULT_MAX_FRAME_SIZE)); const int kMaxFrameSize = 4096; auto config = std::make_shared(TConfiguration::DEFAULT_MAX_MESSAGE_SIZE, kMaxFrameSize); TFramedTransport configured(config); BOOST_CHECK_EQUAL(configured.getMaxFrameSize(), static_cast(kMaxFrameSize)); // The constructors that take a transport already agree; pin that too, so the // three cannot drift apart again. TFramedTransport wrapped(std::make_shared(), config); BOOST_CHECK_EQUAL(wrapped.getMaxFrameSize(), static_cast(kMaxFrameSize)); } // Binding a default to a reference or taking its address needs the library to // define it. Reading the addresses through volatile pointers keeps an // optimising compiler from folding the references away (THRIFT-6213). BOOST_AUTO_TEST_CASE(test_configuration_defaults_are_defined) { const int* volatile maxMessageSize = &TConfiguration::DEFAULT_MAX_MESSAGE_SIZE; const int* volatile maxFrameSize = &TConfiguration::DEFAULT_MAX_FRAME_SIZE; const int* volatile recursionDepth = &TConfiguration::DEFAULT_RECURSION_DEPTH; const TConfiguration config; BOOST_CHECK_EQUAL(*maxMessageSize, config.getMaxMessageSize()); BOOST_CHECK_EQUAL(*maxFrameSize, config.getMaxFrameSize()); BOOST_CHECK_EQUAL(*recursionDepth, config.getRecursionLimit()); }