diff options
author | alyssar@google.com <alyssar@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-03-30 17:50:10 +0000 |
---|---|---|
committer | alyssar@google.com <alyssar@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-03-30 17:50:10 +0000 |
commit | e45d688990a877326888b7ae443c1c3ee8efcfa1 (patch) | |
tree | 5f9174a862ea890f441df425ca82bfac458879ff /net/tools | |
parent | b851da9d3d378df04225d39ea44502742c73c20f (diff) | |
download | chromium_src-e45d688990a877326888b7ae443c1c3ee8efcfa1.zip chromium_src-e45d688990a877326888b7ae443c1c3ee8efcfa1.tar.gz chromium_src-e45d688990a877326888b7ae443c1c3ee8efcfa1.tar.bz2 |
Adding an end to end test for QUIC, and fixing the bugs it turned up.
Review URL: https://chromiumcodereview.appspot.com/13004022
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@191529 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'net/tools')
-rw-r--r-- | net/tools/quic/end_to_end_test.cc | 438 | ||||
-rw-r--r-- | net/tools/quic/quic_in_memory_cache.cc | 2 | ||||
-rw-r--r-- | net/tools/quic/quic_server.cc | 14 | ||||
-rw-r--r-- | net/tools/quic/quic_server.h | 2 | ||||
-rw-r--r-- | net/tools/quic/quic_spdy_server_stream.cc | 1 | ||||
-rw-r--r-- | net/tools/quic/spdy_utils.cc | 34 | ||||
-rw-r--r-- | net/tools/quic/test_tools/http_message_test_utils.cc | 173 | ||||
-rw-r--r-- | net/tools/quic/test_tools/http_message_test_utils.h | 131 | ||||
-rw-r--r-- | net/tools/quic/test_tools/quic_test_client.cc | 181 | ||||
-rw-r--r-- | net/tools/quic/test_tools/quic_test_client.h | 95 | ||||
-rw-r--r-- | net/tools/quic/test_tools/run_all_unittests.cc | 11 |
11 files changed, 1073 insertions, 9 deletions
diff --git a/net/tools/quic/end_to_end_test.cc b/net/tools/quic/end_to_end_test.cc new file mode 100644 index 0000000..68968ca --- /dev/null +++ b/net/tools/quic/end_to_end_test.cc @@ -0,0 +1,438 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <stddef.h> +#include <string> + +#include "base/memory/scoped_ptr.h" +#include "base/memory/singleton.h" +#include "base/strings/string_number_conversions.h" +#include "base/synchronization/waitable_event.h" +#include "base/threading/simple_thread.h" +#include "net/base/ip_endpoint.h" +#include "net/quic/crypto/null_encrypter.h" +#include "net/quic/quic_framer.h" +#include "net/quic/quic_packet_creator.h" +#include "net/quic/quic_protocol.h" +#include "net/quic/test_tools/quic_session_peer.h" +#include "net/quic/test_tools/reliable_quic_stream_peer.h" +#include "net/tools/quic/quic_in_memory_cache.h" +#include "net/tools/quic/quic_server.h" +#include "net/tools/quic/test_tools/http_message_test_utils.h" +#include "net/tools/quic/test_tools/quic_test_client.h" +#include "testing/gtest/include/gtest/gtest.h" + +using base::StringPiece; +using base::WaitableEvent; +using std::string; + +namespace net { +namespace test { +namespace { + +const char* kFooResponseBody = "Artichoke hearts make me happy."; +const char* kBarResponseBody = "Palm hearts are pretty delicious, also."; +const size_t kCongestionFeedbackFrameSize = 25; +// If kCongestionFeedbackFrameSize increase we need to expand this string +// accordingly. +const char* kLargeRequest = + "https://www.google.com/foo/test/a/request/string/longer/than/25/bytes"; + +void GenerateBody(string* body, int length) { + body->clear(); + body->reserve(length); + for (int i = 0; i < length; ++i) { + body->append(1, static_cast<char>(32 + i % (126 - 32))); + } +} + + +// Simple wrapper class to run GFE in a thread. +class ServerThread : public base::SimpleThread { + public: + explicit ServerThread(IPEndPoint address) + : SimpleThread("server_thread"), + listening_(true, false), + quit_(true, false), + address_(address), + port_(0) { + } + virtual ~ServerThread() { + } + + virtual void Run() { + server_.Listen(address_); + + port_lock_.Acquire(); + port_ = server_.port(); + port_lock_.Release(); + + listening_.Signal(); + while (!quit_.IsSignaled()) { + server_.WaitForEvents(); + } + server_.Shutdown(); + } + + int GetPort() { + port_lock_.Acquire(); + int rc = port_; + port_lock_.Release(); + return rc; + } + + WaitableEvent* listening() { return &listening_; } + WaitableEvent* quit() { return &quit_; } + + private: + WaitableEvent listening_; + WaitableEvent quit_; + base::Lock port_lock_; + QuicServer server_; + IPEndPoint address_; + int port_; + + DISALLOW_COPY_AND_ASSIGN(ServerThread); +}; + +class EndToEndTest : public ::testing::Test { + protected: + EndToEndTest() + : server_hostname_("localhost") { + net::IPAddressNumber ip; + CHECK(net::ParseIPLiteralToNumber("127.0.0.1", &ip)); + server_address_ = IPEndPoint(ip, 0); + + AddToCache("GET", kLargeRequest, "HTTP/1.1", "200", "OK", kFooResponseBody); + AddToCache("GET", "https://www.google.com/foo", + "HTTP/1.1", "200", "OK", kFooResponseBody); + AddToCache("GET", "https://www.google.com/bar", + "HTTP/1.1", "200", "OK", kBarResponseBody); + } + + virtual QuicTestClient* CreateQuicClient() { + QuicTestClient* client = new QuicTestClient(server_address_, + server_hostname_); + client->Connect(); + return client; + } + + virtual bool Initialize() { + // Start the server first, because CreateQuicClient() attempts + // to connect to the server. + StartServer(); + client_.reset(CreateQuicClient()); + return client_->client()->connected(); + } + + virtual void TearDown() { + StopServer(); + } + + void StartServer() { + server_thread_.reset(new ServerThread(server_address_)); + server_thread_->Start(); + server_thread_->listening()->Wait(); + server_address_ = IPEndPoint(server_address_.address(), + server_thread_->GetPort()); + } + + void StopServer() { + server_thread_->quit()->Signal(); + server_thread_->Join(); + } + + void AddToCache(const StringPiece& method, + const StringPiece& path, + const StringPiece& version, + const StringPiece& response_code, + const StringPiece& response_detail, + const StringPiece& body) { + BalsaHeaders request_headers, response_headers; + request_headers.SetRequestFirstlineFromStringPieces(method, + path, + version); + response_headers.SetRequestFirstlineFromStringPieces(version, + response_code, + response_detail); + response_headers.AppendHeader("content-length", + base::IntToString(body.length())); + + // Check if response already exists and matches. + QuicInMemoryCache* cache = QuicInMemoryCache::GetInstance(); + const QuicInMemoryCache::Response* cached_response = + cache->GetResponse(request_headers); + if (cached_response != NULL) { + string cached_response_headers_str, response_headers_str; + cached_response->headers().DumpToString(&cached_response_headers_str); + response_headers.DumpToString(&response_headers_str); + CHECK_EQ(cached_response_headers_str, response_headers_str); + CHECK_EQ(cached_response->body(), body); + return; + } + cache->AddResponse(request_headers, response_headers, body); + } + + IPEndPoint server_address_; + string server_hostname_; + scoped_ptr<ServerThread> server_thread_; + scoped_ptr<QuicTestClient> client_; +}; + +TEST_F(EndToEndTest, SimpleRequestResponse) { + ASSERT_TRUE(Initialize()); + + EXPECT_EQ(kFooResponseBody, client_->SendSynchronousRequest("/foo")); + EXPECT_EQ(200ul, client_->response_headers()->parsed_response_code()); +} + +TEST_F(EndToEndTest, SimpleRequestResponsev6) { + IPAddressNumber ip; + CHECK(net::ParseIPLiteralToNumber("::", &ip)); + server_address_ = IPEndPoint(ip, server_address_.port()); + ASSERT_TRUE(Initialize()); + + EXPECT_EQ(kFooResponseBody, client_->SendSynchronousRequest("/foo")); + EXPECT_EQ(200ul, client_->response_headers()->parsed_response_code()); +} + +TEST_F(EndToEndTest, SeparateFinPacket) { + ASSERT_TRUE(Initialize()); + + HTTPMessage request(HttpConstants::HTTP_1_1, + HttpConstants::POST, "/foo"); + request.set_has_complete_message(false); + + client_->SendMessage(request); + + client_->SendData("", true); + + client_->WaitForResponse(); + EXPECT_EQ(kFooResponseBody, client_->response_body()); + EXPECT_EQ(200ul, client_->response_headers()->parsed_response_code()); + + request.AddBody("foo", true); + + client_->SendMessage(request); + client_->SendData("", true); + client_->WaitForResponse(); + EXPECT_EQ(kFooResponseBody, client_->response_body()); + EXPECT_EQ(200ul, client_->response_headers()->parsed_response_code()); +} + +TEST_F(EndToEndTest, MultipleRequestResponse) { + ASSERT_TRUE(Initialize()); + + EXPECT_EQ(kFooResponseBody, client_->SendSynchronousRequest("/foo")); + EXPECT_EQ(200ul, client_->response_headers()->parsed_response_code()); + EXPECT_EQ(kBarResponseBody, client_->SendSynchronousRequest("/bar")); + EXPECT_EQ(200ul, client_->response_headers()->parsed_response_code()); +} + +TEST_F(EndToEndTest, MultipleClients) { + ASSERT_TRUE(Initialize()); + scoped_ptr<QuicTestClient> client2(CreateQuicClient()); + + HTTPMessage request(HttpConstants::HTTP_1_1, + HttpConstants::POST, "/foo"); + request.AddHeader("content-length", "3"); + request.set_has_complete_message(false); + + client_->SendMessage(request); + client2->SendMessage(request); + + client_->SendData("bar", true); + client_->WaitForResponse(); + EXPECT_EQ(kFooResponseBody, client_->response_body()); + EXPECT_EQ(200ul, client_->response_headers()->parsed_response_code()); + + client2->SendData("eep", true); + client2->WaitForResponse(); + EXPECT_EQ(kFooResponseBody, client2->response_body()); + EXPECT_EQ(200ul, client2->response_headers()->parsed_response_code()); +} + +TEST_F(EndToEndTest, RequestOverMultiplePackets) { + ASSERT_TRUE(Initialize()); + // Set things up so we have a small payload, to guarantee fragmentation. + // A congestion feedback frame can't be split into multiple packets, make sure + // that our packet have room for at least this amount after the normal headers + // are added. + + // TODO(rch) handle this better when we have different encryption options. + size_t stream_data = 3; + size_t stream_payload_size = QuicFramer::GetMinStreamFrameSize() + + stream_data; + size_t min_payload_size = + std::max(kCongestionFeedbackFrameSize, stream_payload_size); + size_t ciphertext_size = NullEncrypter().GetCiphertextSize(min_payload_size); + // TODO(satyashekhar): Fix this when versioning is implemented. + client_->options()->max_packet_length = + GetPacketHeaderSize(!kIncludeVersion) + ciphertext_size; + + // Make sure our request is too large to fit in one packet. + EXPECT_GT(strlen(kLargeRequest), min_payload_size); + EXPECT_EQ(kFooResponseBody, client_->SendSynchronousRequest(kLargeRequest)); + EXPECT_EQ(200ul, client_->response_headers()->parsed_response_code()); +} + +TEST_F(EndToEndTest, MultipleFramesRandomOrder) { + ASSERT_TRUE(Initialize()); + // Set things up so we have a small payload, to guarantee fragmentation. + // A congestion feedback frame can't be split into multiple packets, make sure + // that our packet have room for at least this amount after the normal headers + // are added. + + // TODO(rch) handle this better when we have different encryption options. + size_t stream_data = 3; + size_t stream_payload_size = QuicFramer::GetMinStreamFrameSize() + + stream_data; + size_t min_payload_size = + std::max(kCongestionFeedbackFrameSize, stream_payload_size); + size_t ciphertext_size = NullEncrypter().GetCiphertextSize(min_payload_size); + // TODO(satyashekhar): Fix this when versioning is implemented. + client_->options()->max_packet_length = + GetPacketHeaderSize(!kIncludeVersion) + ciphertext_size; + client_->options()->random_reorder = true; + + // Make sure our request is too large to fit in one packet. + EXPECT_GT(strlen(kLargeRequest), min_payload_size); + EXPECT_EQ(kFooResponseBody, client_->SendSynchronousRequest(kLargeRequest)); + EXPECT_EQ(200ul, client_->response_headers()->parsed_response_code()); +} + +TEST_F(EndToEndTest, PostMissingBytes) { + ASSERT_TRUE(Initialize()); + + // Add a content length header with no body. + HTTPMessage request(HttpConstants::HTTP_1_1, + HttpConstants::POST, "/foo"); + request.AddHeader("content-length", "3"); + request.set_skip_message_validation(true); + + // This should be detected as stream fin without complete request, + // triggering an error response. + client_->SendCustomSynchronousRequest(request); + EXPECT_EQ("bad", client_->response_body()); + EXPECT_EQ(500ul, client_->response_headers()->parsed_response_code()); +} + +TEST_F(EndToEndTest, LargePost) { + // FLAGS_fake_packet_loss_percentage = 30; + ASSERT_TRUE(Initialize()); + + string body; + GenerateBody(&body, 10240); + + HTTPMessage request(HttpConstants::HTTP_1_1, + HttpConstants::POST, "/foo"); + request.AddBody(body, true); + + EXPECT_EQ(kFooResponseBody, client_->SendCustomSynchronousRequest(request)); +} + +TEST_F(EndToEndTest, LargePostFEC) { + // FLAGS_fake_packet_loss_percentage = 30; + ASSERT_TRUE(Initialize()); + client_->options()->max_packets_per_fec_group = 6; + + string body; + GenerateBody(&body, 10240); + + HTTPMessage request(HttpConstants::HTTP_1_1, + HttpConstants::POST, "/foo"); + request.AddBody(body, true); + + EXPECT_EQ(kFooResponseBody, client_->SendCustomSynchronousRequest(request)); +} + +/*TEST_F(EndToEndTest, PacketTooLarge) { + FLAGS_quic_allow_oversized_packets_for_test = true; + ASSERT_TRUE(Initialize()); + + string body; + GenerateBody(&body, kMaxPacketSize); + + HTTPMessage request(HttpConstants::HTTP_1_1, + HttpConstants::POST, "/foo"); + request.AddBody(body, true); + client_->options()->max_packet_length = 20480; + + EXPECT_EQ("", client_->SendCustomSynchronousRequest(request)); + EXPECT_EQ(QUIC_STREAM_CONNECTION_ERROR, client_->stream_error()); + EXPECT_EQ(QUIC_PACKET_TOO_LARGE, client_->connection_error()); +}*/ + +TEST_F(EndToEndTest, InvalidStream) { + ASSERT_TRUE(Initialize()); + + string body; + GenerateBody(&body, kMaxPacketSize); + + HTTPMessage request(HttpConstants::HTTP_1_1, + HttpConstants::POST, "/foo"); + request.AddBody(body, true); + // Force the client to write with a stream ID belonging to a nonexistant + // server-side stream. + QuicSessionPeer::SetNextStreamId(2, client_->client()->session()); + + client_->SendCustomSynchronousRequest(request); +// EXPECT_EQ(QUIC_STREAM_CONNECTION_ERROR, client_->stream_error()); + EXPECT_EQ(QUIC_PACKET_FOR_NONEXISTENT_STREAM, client_->connection_error()); +} + +TEST_F(EndToEndTest, MultipleTermination) { + ASSERT_TRUE(Initialize()); + scoped_ptr<QuicTestClient> client2(CreateQuicClient()); + + HTTPMessage request(HttpConstants::HTTP_1_1, + HttpConstants::POST, "/foo"); + request.AddHeader("content-length", "3"); + request.set_has_complete_message(false); + + // Set the offset so we won't frame. Otherwise when we pick up termination + // before HTTP framing is complete, we send an error and close the stream, + // and the second write is picked up as writing on a closed stream. + QuicReliableClientStream* stream = client_->GetOrCreateStream(); + ASSERT_TRUE(stream != NULL); + ReliableQuicStreamPeer::SetStreamBytesWritten(3, stream); + + client_->SendData("bar", true); + + // By default the stream protects itself from writes after terminte is set. + // Override this to test the server handling buggy clients. + ReliableQuicStreamPeer::SetWriteSideClosed( + false, client_->GetOrCreateStream()); + EXPECT_DEBUG_DEATH({ + client_->SendData("eep", true); + client_->WaitForResponse(); + EXPECT_EQ(QUIC_MULTIPLE_TERMINATION_OFFSETS, client_->stream_error()); + }, + "Check failed: !fin_buffered_"); +} + +/*TEST_F(EndToEndTest, Timeout) { + FLAGS_negotiated_timeout_us = 500; + // Note: we do NOT ASSERT_TRUE: we may time out during initial handshake: + // that's enough to validate timeout in this case. + Initialize(); + + while (client_->client()->connected()) { + client_->client()->WaitForEvents(); + } +}*/ + +TEST_F(EndToEndTest, ResetConnection) { + ASSERT_TRUE(Initialize()); + + EXPECT_EQ(kFooResponseBody, client_->SendSynchronousRequest("/foo")); + EXPECT_EQ(200ul, client_->response_headers()->parsed_response_code()); + client_->ResetConnection(); + EXPECT_EQ(kBarResponseBody, client_->SendSynchronousRequest("/bar")); + EXPECT_EQ(200ul, client_->response_headers()->parsed_response_code()); +} + +} // namespace +} // namespace test +} // namespace net diff --git a/net/tools/quic/quic_in_memory_cache.cc b/net/tools/quic/quic_in_memory_cache.cc index 6e1f51f..ede999f 100644 --- a/net/tools/quic/quic_in_memory_cache.cc +++ b/net/tools/quic/quic_in_memory_cache.cc @@ -87,7 +87,7 @@ void QuicInMemoryCache::AddResponse(const BalsaHeaders& request_headers, const BalsaHeaders& response_headers, StringPiece response_body) { LOG(INFO) << "Adding response for: " << GetKey(request_headers); - if (responses_.find(GetKey(request_headers)) == responses_.end()) { + if (ContainsKey(responses_, GetKey(request_headers))) { LOG(DFATAL) << "Response for given request already exists!"; } Response* new_response = new Response(); diff --git a/net/tools/quic/quic_server.cc b/net/tools/quic/quic_server.cc index e97f635..6adee2d 100644 --- a/net/tools/quic/quic_server.cc +++ b/net/tools/quic/quic_server.cc @@ -93,8 +93,20 @@ bool QuicServer::Listen(const IPEndPoint& address) { return false; } - epoll_server_.RegisterFD(fd_, this, kEpollFlags); LOG(INFO) << "Listening on " << address.ToString(); + if (port_ == 0) { + SockaddrStorage storage; + IPEndPoint server_address; + if (getsockname(fd_, storage.addr, &storage.addr_len) != 0 || + !server_address.FromSockAddr(storage.addr, storage.addr_len)) { + LOG(ERROR) << "Unable to get self address. Error: " << strerror(errno); + return false; + } + port_ = server_address.port(); + LOG(INFO) << "Kernel assigned port is " << port_; + } + + epoll_server_.RegisterFD(fd_, this, kEpollFlags); dispatcher_.reset(new QuicDispatcher(fd_, &epoll_server_)); diff --git a/net/tools/quic/quic_server.h b/net/tools/quic/quic_server.h index b985fb5..cc779ea 100644 --- a/net/tools/quic/quic_server.h +++ b/net/tools/quic/quic_server.h @@ -55,6 +55,8 @@ class QuicServer : public EpollCallbackInterface { int packets_dropped() { return packets_dropped_; } + int port() { return port_; } + private: // Accepts data from the framer and demuxes clients to sessions. scoped_ptr<QuicDispatcher> dispatcher_; diff --git a/net/tools/quic/quic_spdy_server_stream.cc b/net/tools/quic/quic_spdy_server_stream.cc index e59c37b..6d98e19 100644 --- a/net/tools/quic/quic_spdy_server_stream.cc +++ b/net/tools/quic/quic_spdy_server_stream.cc @@ -87,6 +87,7 @@ int QuicSpdyServerStream::ParseRequestHeaders() { mutable_body()->append(data + len, delta); } + request_headers_received_ = true; return len; } diff --git a/net/tools/quic/spdy_utils.cc b/net/tools/quic/spdy_utils.cc index f52da10..9078866 100644 --- a/net/tools/quic/spdy_utils.cc +++ b/net/tools/quic/spdy_utils.cc @@ -9,6 +9,7 @@ #include "base/memory/scoped_ptr.h" #include "base/string_piece.h" #include "base/string_util.h" +#include "base/strings/string_number_conversions.h" #include "googleurl/src/gurl.h" #include "net/spdy/spdy_frame_builder.h" #include "net/spdy/spdy_framer.h" @@ -108,12 +109,19 @@ string SpdyUtils::SerializeRequestHeaders( if (request_headers.request_method() == "CONNECT") { path = url; } else { - path = request_uri.path() + "?" + request_uri.query(); - host_and_port = request_uri.port(); + path = request_uri.path(); + if (!request_uri.query().empty()) { + path = path + "?" + request_uri.query(); + } + host_and_port = request_uri.host(); scheme = request_uri.scheme(); } } + DCHECK(!scheme.empty()); + DCHECK(!host_and_port.empty()); + DCHECK(!path.empty()); + SpdyHeaderBlock block; PopulateSpdy3RequestHeaderBlock( request_headers, scheme, host_and_port, path, &block); @@ -170,11 +178,21 @@ bool SpdyUtils::FillBalsaRequestHeaders( request_headers->SetRequestUri(url); request_headers->SetRequestMethod(method_it->second); + BlockIt cl_it = header_block.find("content-length"); + if (cl_it != header_block.end()) { + int content_length; + if (!base::StringToInt(cl_it->second, &content_length)) { + return false; + } + request_headers->SetContentLength(content_length); + } + for (BlockIt it = header_block.begin(); it != header_block.end(); ++it) { if (!IsSpecialSpdyHeader(it, request_headers)) { request_headers->AppendHeader(it->first, it->second); } } + return true; } @@ -188,12 +206,14 @@ bool ParseReasonAndStatus(StringPiece status_and_reason, if (status_and_reason[3] != ' ') return false; - const StringPiece status = StringPiece(status_and_reason.data(), 3); -// if (!IsDigitString(status))//FIXME -// return false; + const StringPiece status_str = StringPiece(status_and_reason.data(), 3); + int status; + if (!base::StringToInt(status_str, &status)) { + return false; + } - headers->SetResponseCode(status); - headers->set_parsed_response_code(atoi(status.data())); + headers->SetResponseCode(status_str); + headers->set_parsed_response_code(status); StringPiece reason(status_and_reason.data() + 4, status_and_reason.length() - 4); diff --git a/net/tools/quic/test_tools/http_message_test_utils.cc b/net/tools/quic/test_tools/http_message_test_utils.cc new file mode 100644 index 0000000..d88d06c --- /dev/null +++ b/net/tools/quic/test_tools/http_message_test_utils.cc @@ -0,0 +1,173 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "net/tools/quic/test_tools/http_message_test_utils.h" + +#include <vector> + +#include "base/basictypes.h" +#include "base/logging.h" +#include "base/strings/string_number_conversions.h" + +using base::StringPiece; +using std::string; +using std::vector; + +namespace net { +namespace test { + +namespace { + +//const char* kContentEncoding = "content-encoding"; +const char* kContentLength = "content-length"; +const char* kTransferCoding = "transfer-encoding"; + +// Both kHTTPVersionString and kMethodString arrays are constructed to match +// the enum values defined in Version and Method of HTTPMessage. +const char* kHTTPVersionString[] = { + "", + "HTTP/0.9", + "HTTP/1.0", + "HTTP/1.1" +}; + +const char* kMethodString[] = { + "", + "OPTIONS", + "GET", + "HEAD", + "POST", + "PUT", + "DELETE", + "TRACE", + "CONNECT", + "MKCOL", + "UNLOCK", +}; + +// Returns true if the message represents a complete request or response. +// Messages are considered complete if: +// - Transfer-Encoding: chunked is present and message has a final chunk. +// - Content-Length header is present and matches the message body length. +// - Neither Transfer-Encoding nor Content-Length is present and message +// is tagged as complete. +bool IsCompleteMessage(const HTTPMessage& message) { + return true; + const BalsaHeaders* headers = message.headers(); + StringPiece content_length = headers->GetHeader(kContentLength); + if (!content_length.empty()) { + int parsed_content_length; + if (!base::StringToInt(content_length, &parsed_content_length)) { + return false; + } + return (message.body().size() == (uint)parsed_content_length); + } else { + // Assume messages without transfer coding or content-length are + // tagged correctly. + return message.has_complete_message(); + } +} + +} // namespace + +HTTPMessage::Method HTTPMessage::StringToMethod(StringPiece str) { + // Skip the first element of the array since it is empty string. + for (unsigned long i = 1; i < arraysize(kMethodString); ++i) { + if (strncmp(str.data(), kMethodString[i], str.length()) == 0) { + return static_cast<HTTPMessage::Method>(i); + } + } + return HttpConstants::UNKNOWN_METHOD; +} + +HTTPMessage::Version HTTPMessage::StringToVersion(StringPiece str) { + // Skip the first element of the array since it is empty string. + for (unsigned long i = 1; i < arraysize(kHTTPVersionString); ++i) { + if (strncmp(str.data(), kHTTPVersionString[i], str.length()) == 0) { + return static_cast<HTTPMessage::Version>(i); + } + } + return HttpConstants::HTTP_UNKNOWN; +} + +const char* HTTPMessage::MethodToString(Method method) { + CHECK_LT(method, arraysize(kMethodString)); + return kMethodString[method]; +} + +const char* HTTPMessage::VersionToString(Version version) { + CHECK_LT(version, arraysize(kHTTPVersionString)); + return kHTTPVersionString[version]; +} + +HTTPMessage::HTTPMessage() + : is_request_(true) { + InitializeFields(); +} + +HTTPMessage::HTTPMessage(Version ver, Method request, const string& path) + : is_request_(true) { + InitializeFields(); + if (ver != HttpConstants::HTTP_0_9) { + headers()->SetRequestVersion(VersionToString(ver)); + } + headers()->SetRequestMethod(MethodToString(request)); + headers()->SetRequestUri(path); +} + +HTTPMessage::~HTTPMessage() { +} + +void HTTPMessage::InitializeFields() { + has_complete_message_ = true; + skip_message_validation_ = false; +} + +void HTTPMessage::AddHeader(const string& header, const string& value) { + headers()->AppendHeader(header, value); +} + +void HTTPMessage::RemoveHeader(const string& header) { + headers()->RemoveAllOfHeader(header); +} + +void HTTPMessage::ReplaceHeader(const string& header, const string& value) { + headers()->ReplaceOrAppendHeader(header, value); +} + +void HTTPMessage::AddBody(const string& body, bool add_content_length) { + body_ = body; + // Remove any transfer-encoding that was left by a previous body. + RemoveHeader(kTransferCoding); + if (add_content_length) { + ReplaceHeader(kContentLength, base::IntToString(body.size())); + } else { + RemoveHeader(kContentLength); + } +} + +void HTTPMessage::ValidateMessage() const { + if (skip_message_validation_) { + return; + } + + vector<StringPiece> transfer_encodings; + headers()->GetAllOfHeader(kTransferCoding, &transfer_encodings); + CHECK_GE(1ul, transfer_encodings.size()); + for (vector<StringPiece>::iterator it = transfer_encodings.begin(); + it != transfer_encodings.end(); + ++it) { + CHECK(StringPieceUtils::EqualIgnoreCase("identity", *it) || + StringPieceUtils::EqualIgnoreCase("chunked", *it)) << *it; + } + + vector<StringPiece> content_lengths; + headers()->GetAllOfHeader(kContentLength, &content_lengths); + CHECK_GE(1ul, content_lengths.size()); + + CHECK_EQ(has_complete_message_, IsCompleteMessage(*this)); +} + +} // namespace test +} // namespace net diff --git a/net/tools/quic/test_tools/http_message_test_utils.h b/net/tools/quic/test_tools/http_message_test_utils.h new file mode 100644 index 0000000..d594295 --- /dev/null +++ b/net/tools/quic/test_tools/http_message_test_utils.h @@ -0,0 +1,131 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef NET_TOOLS_QUIC_TEST_TOOLS_TEST_TOOLS_HTTP_MESSAGE_TEST_UTILS_H_ +#define NET_TOOLS_QUIC_TEST_TOOLS_TEST_TOOLS_HTTP_MESSAGE_TEST_UTILS_H_ + +#include <string> +#include <vector> + +#include "base/string_piece.h" +#include "net/tools/flip_server/balsa_enums.h" +#include "net/tools/flip_server/balsa_headers.h" + +namespace net { +namespace test { + +class HttpConstants { + public: + enum Version { + HTTP_UNKNOWN = 0, + HTTP_0_9, + HTTP_1_0, + HTTP_1_1 + }; + + enum Method { + UNKNOWN_METHOD = 0, + OPTIONS, + GET, + HEAD, + POST, + PUT, + DELETE, + TRACE, + CONNECT, + + MKCOL, + UNLOCK, + }; +}; + +// Stripped down wrapper class which basically contains headers and a body. +class HTTPMessage { + public: + typedef HttpConstants::Version Version; + typedef HttpConstants::Method Method; + + // Convenient functions to map strings into enums. The string passed in is + // not assumed to be NULL-terminated. + static Version StringToVersion(base::StringPiece str); + static Method StringToMethod(base::StringPiece str); + + static const char* MethodToString(Method method); + static const char* VersionToString(Version version); + + // Default constructor makes an empty HTTP/1.1 GET request. This is typically + // used to construct a message that will be Initialize()-ed. + HTTPMessage(); + + // Build a request message + HTTPMessage(Version version, Method request, const std::string& path); + + virtual ~HTTPMessage(); + + const std::string& body() const { return body_; } + + // Adds a header line to the message. + void AddHeader(const std::string& header, const std::string& value); + + // Removes a header line from the message. + void RemoveHeader(const std::string& header); + + // A utility function which calls RemoveHeader followed by AddHeader. + void ReplaceHeader(const std::string& header, const std::string& value); + + // Adds a body and the optional content-length header field (omitted to test + // read until close test case). To generate a message that has a header field + // of 0 content-length, call AddBody("", true). + // Multiple calls to AddBody()/AddChunkedBody() has the effect of overwriting + // the previous entry without warning. + void AddBody(const std::string& body, bool add_content_length); + + bool has_complete_message() const { return has_complete_message_; } + void set_has_complete_message(bool value) { has_complete_message_ = value; } + + // Do some basic http message consistency checks like: + // - Valid transfer-encoding header + // - Valid content-length header + // - Messages we expect to be complete are complete. + // This check can be disabled by setting skip_message_validation. + void ValidateMessage() const; + + bool skip_message_validation() const { return skip_message_validation_; } + void set_skip_message_validation(bool value) { + skip_message_validation_ = value; + } + + // Allow direct access to the body string. This should be used with caution: + // it will not update the request headers like AddBody and AddChunkedBody do. + void set_body(const std::string& body) { body_ = body; } + + const BalsaHeaders* headers() const { return &headers_; } + BalsaHeaders* headers() { return &headers_; } + + protected: + BalsaHeaders headers_; + + std::string body_; // the body with chunked framing/gzip compression + + bool is_request_; + + // True if the message should be considered complete during serialization. + // Used by SPDY and Streamed RPC clients to decide wherever or not + // to include fin flags and during message validation (if enabled). + bool has_complete_message_; + + // Allows disabling message validation when creating test messages + // that are intentionally invalid. + bool skip_message_validation_; + + private: + void InitializeFields(); + + DISALLOW_COPY_AND_ASSIGN(HTTPMessage); +}; + +} // namespace test +} // namespace net + +#endif // NET_TOOLS_QUIC_TEST_TOOLS_TEST_TOOLS_HTTP_MESSAGE_TEST_UTILS_H_ diff --git a/net/tools/quic/test_tools/quic_test_client.cc b/net/tools/quic/test_tools/quic_test_client.cc new file mode 100644 index 0000000..22e3827 --- /dev/null +++ b/net/tools/quic/test_tools/quic_test_client.cc @@ -0,0 +1,181 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "net/tools/quic/test_tools/quic_test_client.h" + +#include "net/tools/flip_server/balsa_headers.h" +#include "net/tools/quic/test_tools/http_message_test_utils.h" + +using std::string; +using base::StringPiece; + +namespace net { +namespace test { + +BalsaHeaders* MungeHeaders(const BalsaHeaders* const_headers) { + StringPiece uri = const_headers->request_uri(); + if (uri.empty()) { + return NULL; + } + if (const_headers->request_method() == "CONNECT") { + return NULL; + } + BalsaHeaders* headers = new BalsaHeaders; + headers->CopyFrom(*const_headers); + if (!uri.starts_with("https://") && + !uri.starts_with("http://")) { + // If we have a relative URL, set some defaults. + string full_uri = "https:/www.google.com"; + full_uri.append(uri.as_string()); + headers->SetRequestUri(full_uri); + } + return headers; +} + +QuicTestClient::QuicTestClient(IPEndPoint address, const string& hostname) + : server_address_(address), + client_(address, hostname), + stream_(NULL), + stream_error_(QUIC_NO_ERROR), + connection_error_(QUIC_NO_ERROR), + bytes_read_(0), + bytes_written_(0), + never_connected_(true) { +} + +QuicTestClient::~QuicTestClient() { + if (stream_) { + stream_->set_visitor(NULL); + } +} + +ssize_t QuicTestClient::SendRequest(const string& uri) { + HTTPMessage message(HttpConstants::HTTP_1_1, HttpConstants::GET, uri); + return SendMessage(message); +} + +ssize_t QuicTestClient::SendMessage(const HTTPMessage& message) { + stream_ = NULL; // Always force creation of a stream for SendMessage. + + QuicReliableClientStream* stream = GetOrCreateStream(); + if (!stream) { return 0; } + scoped_ptr<BalsaHeaders> munged_headers(MungeHeaders(message.headers())); + return GetOrCreateStream()->SendRequest( + munged_headers.get() ? *munged_headers.get() : *message.headers(), + message.body(), + message.has_complete_message()); +} + +ssize_t QuicTestClient::SendData(string data, bool last_data) { + QuicReliableClientStream* stream = GetOrCreateStream(); + if (!stream) { return 0; } + GetOrCreateStream()->SendBody(data, last_data); + return data.length(); +} + +string QuicTestClient::SendCustomSynchronousRequest( + const HTTPMessage& message) { + SendMessage(message); + WaitForResponse(); + return response_; +} + +string QuicTestClient::SendSynchronousRequest(const string& uri) { + if (SendRequest(uri) == 0) { + DLOG(ERROR) << "Failed to the request for uri:" << uri; + return ""; + } + WaitForResponse(); + return response_; +} + +QuicReliableClientStream* QuicTestClient::GetOrCreateStream() { + if (never_connected_ == true) { + if (!connected()) { + Connect(); + } + if (!connected()) { + return NULL; + } + } + if (!stream_) { + stream_ = client_.CreateReliableClientStream(); + stream_->set_visitor(this); + } + return stream_; +} + +bool QuicTestClient::connected() const { + return client_.connected(); +} + +void QuicTestClient::WaitForResponse() { + client_.WaitForStreamToClose(stream_->id()); +} + +void QuicTestClient::Connect() { + DCHECK(!connected()); + client_.Initialize(); + client_.Connect(); + never_connected_ = false; +} + +void QuicTestClient::ResetConnection() { + Disconnect(); + Connect(); +} + +void QuicTestClient::Disconnect() { + client_.Disconnect(); +} + +IPEndPoint QuicTestClient::LocalSocketAddress() const { + return client_.client_address(); +} + +void QuicTestClient::ClearPerRequestState() { + stream_error_ = QUIC_NO_ERROR; + connection_error_ = QUIC_NO_ERROR; + stream_ = NULL; + response_ = ""; + headers_.Clear(); + bytes_read_ = 0; + bytes_written_ = 0; +} + +void QuicTestClient::WaitForInitialResponse() { + DCHECK(stream_ != NULL); + while (stream_ && stream_->stream_bytes_read() == 0) { + client_.WaitForEvents(); + } +} + +ssize_t QuicTestClient::Send(const void *buffer, size_t size) { + return SendData(string(static_cast<const char*>(buffer), size), false); +} + +int QuicTestClient::response_size() const { + return bytes_read_; +} + +size_t QuicTestClient::bytes_read() const { + return bytes_read_; +} + +size_t QuicTestClient::bytes_written() const { + return bytes_written_; +} + +void QuicTestClient::OnClose(ReliableQuicStream* stream) { + response_ = stream_->data(); + headers_.CopyFrom(stream_->headers()); + stream_error_ = stream_->error(); + connection_error_ = stream_->error(); + bytes_read_ = stream_->stream_bytes_read(); + bytes_written_ = stream_->stream_bytes_written(); + stream_ = NULL; +} + +} // namespace test +} // namespace net diff --git a/net/tools/quic/test_tools/quic_test_client.h b/net/tools/quic/test_tools/quic_test_client.h new file mode 100644 index 0000000..d02905a --- /dev/null +++ b/net/tools/quic/test_tools/quic_test_client.h @@ -0,0 +1,95 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef NET_QUIC_TEST_TOOLS_QUIC_CLIENT_H_ +#define NET_QUIC_TEST_TOOLS_QUIC_CLIENT_H_ + +#include <string> + +#include "base/basictypes.h" +#include "net/quic/quic_framer.h" +#include "net/quic/quic_packet_creator.h" +#include "net/quic/quic_protocol.h" +#include "net/tools/quic/quic_client.h" + +namespace net { + +namespace test { + +class HTTPMessage; + +// A toy QUIC client used for testing. +class QuicTestClient : public ReliableQuicStream::Visitor { + public: + QuicTestClient(IPEndPoint server_address, const string& server_hostname); + virtual ~QuicTestClient(); + + // Clears any outstanding state and sends a simple GET of 'uri' to the + // server. Returns 0 if the request failed and no bytes were written. + ssize_t SendRequest(const string& uri); + ssize_t SendMessage(const HTTPMessage& message); + + string SendCustomSynchronousRequest(const HTTPMessage& message); + string SendSynchronousRequest(const string& uri); + + // Wraps data in a quic packet and sends it. + ssize_t SendData(string data, bool last_data); + + QuicPacketCreator::Options* options() { return client_.options(); } + + const BalsaHeaders *response_headers() const {return &headers_;} + + void WaitForResponse(); + + void Connect(); + void ResetConnection(); + void Disconnect(); + IPEndPoint LocalSocketAddress() const; + void ClearPerRequestState(); + void WaitForInitialResponse(); + ssize_t Send(const void *buffer, size_t size); + int response_size() const; + size_t bytes_read() const; + size_t bytes_written() const; + + // From ReliableQuicStream::Visitor + virtual void OnClose(ReliableQuicStream* stream) OVERRIDE; + + void SetNextStreamId(QuicStreamId id); + + // Returns NULL if the maximum number of streams have already been created. + QuicReliableClientStream* GetOrCreateStream(); + + QuicErrorCode stream_error() { return stream_error_; } + QuicErrorCode connection_error() { return connection_error_; } + + QuicClient* client() { return &client_; } + + const string& response_body() {return response_;} + bool connected() const; + + private: + IPEndPoint server_address_; + IPEndPoint client_address_; + QuicClient client_; // The actual client + QuicReliableClientStream* stream_; + + QuicErrorCode stream_error_; + QuicErrorCode connection_error_; + + BalsaHeaders headers_; + string response_; + uint64 bytes_read_; + uint64 bytes_written_; + // True if the client has never connected before. The client will + // auto-connect exactly once before sending data. If something causes a + // connection reset, it will not automatically reconnect. + bool never_connected_; +}; + +} // namespace test + +} // namespace net + +#endif // NET_QUIC_TEST_TOOLS_QUIC_CLIENT_H_ diff --git a/net/tools/quic/test_tools/run_all_unittests.cc b/net/tools/quic/test_tools/run_all_unittests.cc new file mode 100644 index 0000000..6d42d33 --- /dev/null +++ b/net/tools/quic/test_tools/run_all_unittests.cc @@ -0,0 +1,11 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "base/test/test_suite.h" + +int main(int argc, char** argv) { + base::TestSuite test_suite(argc, argv); + + return test_suite.Run(); +} |