blob: 65c8749944b040b4caafba4b40502d45ee6147d1 [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 <cstdint>
#include <cstring>
#include <deque>
#include <memory>
#include <string>
#include <thrift/TConfiguration.h>
#include <thrift/transport/TVirtualTransport.h>
#include <thrift/transport/TWebSocketServer.h>
/*
* A WebSocket frame header carries the payload length, and readFrame() used it
* to size the read buffer before any payload byte had arrived. Fourteen bytes
* on the wire therefore decided how much memory the server committed, up to
* the 4 GiB a 64-bit length field can name.
*
* These tests hold the declared length to TConfiguration::maxFrameSize, the
* same ceiling TFramedTransport applies to its own frames. What they measure
* is the largest read the server asked of the transport underneath it: a frame
* that was refused before the allocation never asks for its payload, while one
* that was believed asks for all of it whether or not it ever arrives. Asking
* only "did the read fail?" would pass on the unmodified library too, because
* a payload that never arrives ends the frame either way.
*/
BOOST_AUTO_TEST_SUITE(TWebSocketServerTest)
using apache::thrift::TConfiguration;
using apache::thrift::transport::TTransport;
using apache::thrift::transport::TVirtualTransport;
using apache::thrift::transport::TWebSocketServer;
namespace {
// A transport whose inbound side is a script of chunks: one read() is served
// from one chunk, so a test says exactly how the peer's bytes arrive. It also
// remembers the largest read it was asked for, which is what distinguishes a
// declared length that was believed from one that was not.
class ScriptedTransport : public TVirtualTransport<ScriptedTransport> {
public:
void feed(const std::string& chunk) { inbound_.push_back(chunk); }
uint32_t read(uint8_t* buf, uint32_t len) {
largestRead_ = (std::max)(largestRead_, len);
noteStackDepth();
if (inbound_.empty()) {
return 0;
}
std::string& front = inbound_.front();
auto give = (std::min)(static_cast<size_t>(len), front.size());
std::memcpy(buf, front.data(), give);
front.erase(0, give);
if (front.empty()) {
inbound_.pop_front();
}
return static_cast<uint32_t>(give);
}
void write(const uint8_t* buf, uint32_t len) {
outbound_.append(reinterpret_cast<const char*>(buf), len);
}
void open() override { open_ = true; }
bool isOpen() const override { return open_; }
void close() override { open_ = false; }
uint32_t largestRead() const { return largestRead_; }
const std::string& outbound() const { return outbound_; }
// How far below the caller's own frame the reader got. A reader that
// re-enters itself per frame shows up here and nowhere else: the frames it
// consumes and the bytes it hands over are the same either way.
void anchorStack(const void* here) {
anchor_ = reinterpret_cast<uintptr_t>(here);
deepestStack_ = 0;
}
size_t deepestStack() const { return deepestStack_; }
private:
void noteStackDepth() {
if (anchor_ == 0) {
return;
}
char here;
auto now = reinterpret_cast<uintptr_t>(&here);
size_t depth = (now > anchor_) ? (now - anchor_) : (anchor_ - now);
deepestStack_ = (std::max)(deepestStack_, depth);
}
std::deque<std::string> inbound_;
std::string outbound_;
uint32_t largestRead_ = 0;
uintptr_t anchor_ = 0;
size_t deepestStack_ = 0;
bool open_ = true;
};
const char* kHandshake
= "GET / HTTP/1.1\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
"Sec-WebSocket-Version: 13\r\n"
"\r\n";
// Everything the handshake itself reads: refill() asks for the initial HTTP
// buffer, and the frame header reads that follow ask for 2, 8 and 4 bytes.
// Any read larger than this came from a declared payload length.
const uint32_t kHandshakeReadCeiling = 1024;
const uint8_t kMask[4] = {0x37, 0xFA, 0x21, 0x3D};
void appendBigEndian(std::string& out, uint64_t value, int bytes) {
for (int i = bytes - 1; i >= 0; --i) {
out.push_back(static_cast<char>((value >> (i * 8)) & 0xFF));
}
}
// A masked client frame declaring `declaredLength` bytes of payload and
// actually carrying `payload`. The two differ in the tests where the point is
// that the declaration alone must not be acted on.
std::string clientFrame(uint64_t declaredLength, const std::string& payload, uint8_t opcode = 0x2) {
std::string frame;
frame.push_back(static_cast<char>(0x80 | opcode)); // FIN
if (declaredLength < 126) {
frame.push_back(static_cast<char>(0x80 | declaredLength)); // MASK
} else if (declaredLength < 65536) {
frame.push_back(static_cast<char>(0x80 | 126));
appendBigEndian(frame, declaredLength, 2);
} else {
frame.push_back(static_cast<char>(0x80 | 127));
appendBigEndian(frame, declaredLength, 8);
}
frame.append(reinterpret_cast<const char*>(kMask), 4);
for (size_t i = 0; i < payload.size(); ++i) {
frame.push_back(static_cast<char>(payload[i] ^ kMask[i % 4]));
}
return frame;
}
std::shared_ptr<TConfiguration> withMaxFrameSize(int maxFrameSize) {
return std::make_shared<TConfiguration>(TConfiguration::DEFAULT_MAX_MESSAGE_SIZE, maxFrameSize);
}
// The server is handed back as a TTransport, which is how a transport factory
// hands it to a protocol: readAll() on the concrete type resolves to the CRTP
// helper in TVirtualTransport and would never reach the WebSocket framing.
std::shared_ptr<ScriptedTransport> connect(std::shared_ptr<TTransport>* server,
std::shared_ptr<TConfiguration> config = nullptr) {
auto inner = std::make_shared<ScriptedTransport>();
inner->feed(kHandshake);
*server = std::make_shared<TWebSocketServer<true> >(inner, config);
return inner;
}
// True if the last frame written to the peer is a Close carrying `code`. The
// length byte is deliberately not asserted on: what matters here is that the
// peer was told why the connection ended.
bool closedWith(const std::string& outbound, uint16_t code) {
if (outbound.size() < 4) {
return false;
}
const auto* tail = reinterpret_cast<const uint8_t*>(outbound.data()) + outbound.size() - 4;
return tail[0] == 0x88 && ((tail[2] << 8) | tail[3]) == code;
}
const uint16_t kMessageTooBig = 1009;
// Everything the server wrote after the 101 that ends the handshake.
std::string framesWritten(const std::string& outbound) {
auto end = outbound.find("\r\n\r\n");
return end == std::string::npos ? outbound : outbound.substr(end + 4);
}
std::string hex(const std::string& bytes) {
static const char* digits = "0123456789ABCDEF";
std::string out;
for (size_t i = 0; i < bytes.size(); ++i) {
auto b = static_cast<unsigned char>(bytes[i]);
if (i > 0) {
out.push_back(' ');
}
out.push_back(digits[b >> 4]);
out.push_back(digits[b & 0x0F]);
}
return out;
}
const uint8_t kPingOpcode = 0x9;
// Enough pings to separate a reader that recurses from one that does not, and
// far too few to run any stack out: at the ~160 bytes a frame costs, the
// unmodified reader passes 300 kB here while the whole run stays four orders of
// magnitude inside the eight megabytes a Linux thread starts with.
const int kPings = 2000;
const size_t kStackCeiling = 64 * 1024;
} // namespace
BOOST_AUTO_TEST_CASE(a_frame_over_the_default_maximum_is_refused) {
// Sixty-four megabytes declared, fourteen bytes sent. The library default
// for maxFrameSize is 16 MB, so this frame has to be refused on its header.
std::shared_ptr<TTransport> server;
auto inner = connect(&server);
inner->feed(clientFrame(64 * 1024 * 1024, ""));
uint8_t out[16];
BOOST_CHECK_EQUAL(server->readAll(out, sizeof(out)), 0u);
BOOST_CHECK_LE(inner->largestRead(), kHandshakeReadCeiling);
BOOST_CHECK(closedWith(inner->outbound(), kMessageTooBig));
}
BOOST_AUTO_TEST_CASE(the_bound_is_the_configured_maximum) {
// An operator who lowers maxFrameSize gets the lower bound, and one who
// raises it gets the higher one. 4 KiB is far below any default, so a frame
// of 32 KiB tells the two apart.
std::shared_ptr<TTransport> server;
auto inner = connect(&server, withMaxFrameSize(4096));
inner->feed(clientFrame(32 * 1024, ""));
uint8_t out[16];
BOOST_CHECK_EQUAL(server->readAll(out, sizeof(out)), 0u);
BOOST_CHECK_LE(inner->largestRead(), kHandshakeReadCeiling);
BOOST_CHECK(closedWith(inner->outbound(), kMessageTooBig));
}
BOOST_AUTO_TEST_CASE(a_frame_of_the_maximum_size_is_still_accepted) {
// The bound is a maximum, not a limit one below it: a frame of exactly
// maxFrameSize still has to be read.
const uint32_t kMax = 4096;
std::shared_ptr<TTransport> server;
auto inner = connect(&server, withMaxFrameSize(kMax));
std::string payload(kMax, 'x');
inner->feed(clientFrame(kMax, payload));
std::string got(kMax, '\0');
BOOST_CHECK_EQUAL(server->readAll(reinterpret_cast<uint8_t*>(&got[0]), kMax), kMax);
BOOST_CHECK_EQUAL(got, payload);
}
BOOST_AUTO_TEST_CASE(an_ordinary_frame_still_reads) {
// The regression guard: a small frame is unmasked and handed over whole.
std::shared_ptr<TTransport> server;
auto inner = connect(&server);
const std::string payload = "the quick brown fox jumps over the lazy dog";
inner->feed(clientFrame(payload.size(), payload));
std::string got(payload.size(), '\0');
BOOST_CHECK_EQUAL(server->readAll(reinterpret_cast<uint8_t*>(&got[0]),
static_cast<uint32_t>(payload.size())),
payload.size());
BOOST_CHECK_EQUAL(got, payload);
}
BOOST_AUTO_TEST_CASE(consecutive_frames_still_read) {
// Each frame is bounded on its own, so a second one has to be readable after
// the first.
std::shared_ptr<TTransport> server;
auto inner = connect(&server);
inner->feed(clientFrame(5, "alpha"));
inner->feed(clientFrame(4, "beta"));
char got[5];
BOOST_CHECK_EQUAL(server->readAll(reinterpret_cast<uint8_t*>(got), 5), 5u);
BOOST_CHECK_EQUAL(std::string(got, 5), "alpha");
BOOST_CHECK_EQUAL(server->readAll(reinterpret_cast<uint8_t*>(got), 4), 4u);
BOOST_CHECK_EQUAL(std::string(got, 4), "beta");
}
BOOST_AUTO_TEST_CASE(a_payload_split_across_reads_still_arrives_whole) {
// The transport underneath hands over one chunk per read, which is what a
// socket does with anything larger than a segment. The frame is ordinary and
// well inside every limit; the only thing unusual about it is that it does
// not arrive all at once.
std::shared_ptr<TTransport> server;
auto inner = connect(&server);
const std::string payload(4000, 'A');
const std::string frame = clientFrame(payload.size(), payload);
inner->feed(frame.substr(0, 8));
inner->feed(frame.substr(8, 1000));
inner->feed(frame.substr(1008));
std::string got(payload.size(), '\0');
BOOST_CHECK_EQUAL(server->readAll(reinterpret_cast<uint8_t*>(&got[0]),
static_cast<uint32_t>(payload.size())),
payload.size());
BOOST_CHECK_EQUAL(got, payload);
}
BOOST_AUTO_TEST_CASE(a_payload_that_stops_early_is_still_end_of_stream) {
// Waiting for the rest of a frame must not turn a peer that went away into a
// half-read frame handed to the protocol.
std::shared_ptr<TTransport> server;
auto inner = connect(&server);
const std::string payload(4000, 'A');
const std::string frame = clientFrame(payload.size(), payload);
inner->feed(frame.substr(0, 500));
uint8_t out[16];
BOOST_CHECK_EQUAL(server->readAll(out, sizeof(out)), 0u);
}
BOOST_AUTO_TEST_CASE(a_run_of_pings_is_answered_from_one_stack_frame) {
// A Ping carries nothing for the caller, so the reader has to go on to the
// next frame to satisfy it. Doing that by re-entering itself costs a stack
// frame per Ping, and a Ping is seven bytes on the wire.
std::shared_ptr<TTransport> server;
auto inner = connect(&server);
std::string wire;
for (int i = 0; i < kPings; ++i) {
wire += clientFrame(1, "x", kPingOpcode);
}
wire += clientFrame(5, "hello");
inner->feed(wire);
char anchor;
inner->anchorStack(&anchor);
char got[5];
BOOST_CHECK_EQUAL(server->readAll(reinterpret_cast<uint8_t*>(got), 5), 5u);
BOOST_CHECK_EQUAL(std::string(got, 5), "hello");
BOOST_CHECK_LE(inner->deepestStack(), kStackCeiling);
}
BOOST_AUTO_TEST_CASE(an_empty_masked_frame_does_not_desynchronise_the_stream) {
// The masking key is present in every frame whose MASK bit is set, whatever
// the payload length, and this server refuses a client frame that does not
// set it. Leaving those four bytes in the stream makes the next header be
// parsed out of the masking key.
std::shared_ptr<TTransport> server;
auto inner = connect(&server);
inner->feed(clientFrame(0, "", kPingOpcode) + clientFrame(5, "hello"));
char got[5];
BOOST_CHECK_EQUAL(server->readAll(reinterpret_cast<uint8_t*>(got), 5), 5u);
BOOST_CHECK_EQUAL(std::string(got, 5), "hello");
}
BOOST_AUTO_TEST_CASE(a_pong_declares_the_length_it_carries) {
// RFC 6455 wants a Pong to carry the Ping's payload, and the header has to
// describe it, or the peer reads the payload as the next frame header.
std::shared_ptr<TTransport> server;
auto inner = connect(&server);
inner->feed(clientFrame(8, "PINGDATA", kPingOpcode) + clientFrame(5, "hello"));
char got[5];
BOOST_CHECK_EQUAL(server->readAll(reinterpret_cast<uint8_t*>(got), 5), 5u);
BOOST_CHECK_EQUAL(hex(framesWritten(inner->outbound())), "8A 08 50 49 4E 47 44 41 54 41");
}
BOOST_AUTO_TEST_CASE(a_pong_for_an_empty_ping_carries_nothing) {
// The Pong echoes the read buffer, so a frame with no payload has to leave
// that buffer empty rather than whatever the frame before it left unread.
std::shared_ptr<TTransport> server;
auto inner = connect(&server);
inner->feed(clientFrame(5, "hello") + clientFrame(0, "", kPingOpcode) + clientFrame(5, "world"));
char got[1];
BOOST_CHECK_EQUAL(server->readAll(reinterpret_cast<uint8_t*>(got), 1), 1u);
BOOST_CHECK_EQUAL(got[0], 'h');
// Four bytes of "hello" are still unread when the Ping arrives.
BOOST_CHECK_EQUAL(server->readAll(reinterpret_cast<uint8_t*>(got), 1), 1u);
BOOST_CHECK_EQUAL(got[0], 'e');
char rest[3];
BOOST_CHECK_EQUAL(server->readAll(reinterpret_cast<uint8_t*>(rest), 3), 3u);
char after[5];
BOOST_CHECK_EQUAL(server->readAll(reinterpret_cast<uint8_t*>(after), 5), 5u);
BOOST_CHECK_EQUAL(std::string(after, 5), "world");
BOOST_CHECK_EQUAL(hex(framesWritten(inner->outbound())), "8A 00");
}
BOOST_AUTO_TEST_CASE(a_response_frame_still_declares_its_own_length) {
// flush() is the one caller whose body really is the write buffer, and it
// has to keep saying so.
std::shared_ptr<TTransport> server;
auto inner = connect(&server);
inner->feed(clientFrame(5, "hello"));
char got[5];
BOOST_CHECK_EQUAL(server->readAll(reinterpret_cast<uint8_t*>(got), 5), 5u);
const std::string response = "world";
server->write(reinterpret_cast<const uint8_t*>(response.data()),
static_cast<uint32_t>(response.size()));
server->flush();
BOOST_CHECK_EQUAL(hex(framesWritten(inner->outbound())), "82 05 77 6F 72 6C 64");
}
BOOST_AUTO_TEST_CASE(a_close_declares_the_length_it_carries) {
// The close code is unreadable to a conforming peer if the header in front
// of it says the frame is empty.
std::shared_ptr<TTransport> server;
auto inner = connect(&server, withMaxFrameSize(4096));
inner->feed(clientFrame(32 * 1024, ""));
uint8_t out[16];
BOOST_CHECK_EQUAL(server->readAll(out, sizeof(out)), 0u);
BOOST_CHECK_EQUAL(hex(framesWritten(inner->outbound())), "88 02 03 F1");
}
BOOST_AUTO_TEST_CASE(a_length_with_the_high_bit_set_is_still_refused) {
// Unchanged behaviour, kept here so the new bound cannot be mistaken for the
// only thing standing between the header and the allocation.
std::shared_ptr<TTransport> server;
auto inner = connect(&server);
inner->feed(clientFrame(0x8000000000000000ULL, ""));
uint8_t out[16];
BOOST_CHECK_THROW(server->readAll(out, sizeof(out)),
apache::thrift::transport::TTransportException);
BOOST_CHECK_LE(inner->largestRead(), kHandshakeReadCeiling);
}
BOOST_AUTO_TEST_SUITE_END()