You are viewing a plain text version of this content. The canonical link for it is here.
Posted to cvs@httpd.apache.org by ji...@apache.org on 2014/05/01 13:39:32 UTC
svn commit: r1591620 [3/14] - in /httpd/mod_spdy/branches/httpd-2.2.x: ./
base/ base/metrics/ build/ install/ install/common/ install/debian/
install/rpm/ mod_spdy/ mod_spdy/apache/ mod_spdy/apache/filters/
mod_spdy/apache/testing/ mod_spdy/common/ mod...
Added: httpd/mod_spdy/branches/httpd-2.2.x/mod_spdy/apache/filters/http_to_spdy_filter.cc
URL: http://svn.apache.org/viewvc/httpd/mod_spdy/branches/httpd-2.2.x/mod_spdy/apache/filters/http_to_spdy_filter.cc?rev=1591620&view=auto
==============================================================================
--- httpd/mod_spdy/branches/httpd-2.2.x/mod_spdy/apache/filters/http_to_spdy_filter.cc (added)
+++ httpd/mod_spdy/branches/httpd-2.2.x/mod_spdy/apache/filters/http_to_spdy_filter.cc Thu May 1 11:39:27 2014
@@ -0,0 +1,223 @@
+// Copyright 2010 Google Inc.
+//
+// Licensed 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.
+
+// There are a number of things that every output filter should do, according
+// to <http://httpd.apache.org/docs/2.3/developer/output-filters.html>. In
+// short, these things are:
+//
+// - Respect FLUSH and EOS metadata buckets, and pass other metadata buckets
+// down the chain. Ignore all buckets after an EOS.
+//
+// - Don't allocate long-lived memory on every invocation. In particular, if
+// you need a temp brigade, allocate it once and then reuse it each time.
+//
+// - Never pass an empty brigade down the chain, but be ready to accept one
+// and do nothing.
+//
+// - Calling apr_brigade_destroy can be dangerous; prefer using
+// apr_brigade_cleanup instead.
+//
+// - Don't read the entire brigade into memory at once; the brigade may, for
+// example, contain a FILE bucket representing a 42 GB file. Instead, use
+// apr_bucket_read to read a reasonable portion of the bucket, put the
+// resulting (small) bucket into a temp brigade, pass it down the chain,
+// and then clean up the temp brigade before continuing.
+//
+// - If a bucket is to be saved beyond the scope of the filter invocation
+// that first received it, it must be "set aside" using the
+// apr_bucket_setaside macro.
+//
+// - When reading a bucket, first use a non-blocking read; if it fails with
+// APR_EAGAIN, send a FLUSH bucket down the chain, and then read the bucket
+// with a blocking read.
+//
+// This code attempts to follow these rules.
+
+#include "mod_spdy/apache/filters/http_to_spdy_filter.h"
+
+#include "apr_strings.h"
+
+#include "base/logging.h"
+#include "mod_spdy/apache/pool_util.h" // for AprStatusString
+#include "mod_spdy/common/protocol_util.h"
+#include "mod_spdy/common/spdy_server_config.h"
+#include "mod_spdy/common/spdy_stream.h"
+#include "mod_spdy/common/version.h"
+#include "net/spdy/spdy_protocol.h"
+
+namespace {
+
+const char* kModSpdyVersion = MOD_SPDY_VERSION_STRING "-" LASTCHANGE_STRING;
+
+} // namespace
+
+namespace mod_spdy {
+
+HttpToSpdyFilter::HttpToSpdyFilter(const SpdyServerConfig* config,
+ SpdyStream* stream)
+ : receiver_(config, stream),
+ converter_(stream->spdy_version(), &receiver_),
+ eos_bucket_received_(false) {}
+
+HttpToSpdyFilter::~HttpToSpdyFilter() {}
+
+// Check if the SPDY stream has been aborted; if so, mark the connection object
+// as having been aborted and return APR_ECONNABORTED. Hopefully, this will
+// convince Apache to shut down processing for this (slave) connection, thus
+// allowing this stream's thread to complete and exit.
+#define RETURN_IF_STREAM_ABORT(filter) \
+ do { \
+ if ((filter)->c->aborted || receiver_.stream_->is_aborted()) { \
+ (filter)->c->aborted = true; \
+ return APR_ECONNABORTED; \
+ } \
+ } while (false)
+
+apr_status_t HttpToSpdyFilter::Write(ap_filter_t* filter,
+ apr_bucket_brigade* input_brigade) {
+ // This is a NETWORK-level filter, so there shouldn't be any filter after us.
+ if (filter->next != NULL) {
+ LOG(WARNING) << "HttpToSpdyFilter is not the last filter in the chain "
+ << "(it is followed by " << filter->next->frec->name << ")";
+ }
+
+ // According to the page at
+ // http://httpd.apache.org/docs/2.3/developer/output-filters.html
+ // we should never pass an empty brigade down the chain, but to be safe, we
+ // should be prepared to accept one and do nothing.
+ if (APR_BRIGADE_EMPTY(input_brigade)) {
+ LOG(INFO) << "HttpToSpdyFilter received an empty brigade.";
+ return APR_SUCCESS;
+ }
+
+ // Loop through the brigade, reading and sending data. We delete each bucket
+ // once we have successfully consumed it, before moving on to the next
+ // bucket. There are two reasons to delete buckets as we go:
+ //
+ // 1) Some output filters (such as mod_deflate) that come before us will
+ // expect us to empty out the brigade that they give us before we
+ // return. If we don't do so, the second time they call us we'll see
+ // all those same buckets again (along with the new buckets).
+ //
+ // 2) Some bucket types such as FILE don't store their data in memory, and
+ // when read, split into two buckets: one containing some data, and the
+ // other representing the rest of the file. If we read in all buckets
+ // in the brigade without deleting ones we're done with, we will
+ // eventually read the whole file into memory; by deleting buckets as we
+ // go, only a portion of the file is in memory at a time.
+ while (!APR_BRIGADE_EMPTY(input_brigade)) {
+ apr_bucket* bucket = APR_BRIGADE_FIRST(input_brigade);
+
+ if (APR_BUCKET_IS_METADATA(bucket)) {
+ if (APR_BUCKET_IS_EOS(bucket)) {
+ // EOS bucket -- there should be no more data buckets in this stream.
+ eos_bucket_received_ = true;
+ RETURN_IF_STREAM_ABORT(filter);
+ converter_.Flush();
+ } else if (APR_BUCKET_IS_FLUSH(bucket)) {
+ // FLUSH bucket -- call Send() immediately and flush the data buffer.
+ RETURN_IF_STREAM_ABORT(filter);
+ converter_.Flush();
+ } else {
+ // Unknown metadata bucket. This bucket has no meaning to us, and
+ // there's no further filter to pass it to, so we just ignore it.
+ }
+ } else if (eos_bucket_received_) {
+ // We shouldn't be getting any data buckets after an EOS (since this is a
+ // connection-level filter, we do sometimes see other metadata buckets
+ // after the EOS). If we do get them, ignore them.
+ LOG(INFO) << "HttpToSpdyFilter received " << bucket->type->name
+ << " bucket after an EOS (and ignored it).";
+ } else {
+ // Data bucket -- get ready to read.
+ const char* data = NULL;
+ apr_size_t data_length = 0;
+
+ // First, try a non-blocking read.
+ apr_status_t status = apr_bucket_read(bucket, &data, &data_length,
+ APR_NONBLOCK_READ);
+ if (status == APR_SUCCESS) {
+ RETURN_IF_STREAM_ABORT(filter);
+ if (!converter_.ProcessInput(data, static_cast<size_t>(data_length))) {
+ // Parse failure. The parser will have already logged an error.
+ return APR_EGENERAL;
+ }
+ } else if (APR_STATUS_IS_EAGAIN(status)) {
+ // Non-blocking read failed with EAGAIN, so try again with a blocking
+ // read (but flush first, in case we block for a long time).
+ RETURN_IF_STREAM_ABORT(filter);
+ converter_.Flush();
+ status = apr_bucket_read(bucket, &data, &data_length, APR_BLOCK_READ);
+ if (status != APR_SUCCESS) {
+ LOG(ERROR) << "Blocking read failed with status " << status << ": "
+ << AprStatusString(status);
+ // Since we didn't successfully consume this bucket, don't delete it;
+ // rather, leave it (and any remaining buckets) in the brigade.
+ return status; // failure
+ }
+ RETURN_IF_STREAM_ABORT(filter);
+ if (!converter_.ProcessInput(data, static_cast<size_t>(data_length))) {
+ // Parse failure. The parser will have already logged an error.
+ return APR_EGENERAL;
+ }
+ } else {
+ // Since we didn't successfully consume this bucket, don't delete it;
+ // rather, leave it (and any remaining buckets) in the brigade.
+ return status; // failure
+ }
+ }
+
+ // We consumed this bucket successfully, so delete it and move on to the
+ // next.
+ apr_bucket_delete(bucket);
+ }
+
+ // We went through the whole brigade successfully, so it must be empty when
+ // we return (see http://code.google.com/p/mod-spdy/issues/detail?id=17).
+ DCHECK(APR_BRIGADE_EMPTY(input_brigade));
+ return APR_SUCCESS;
+}
+
+HttpToSpdyFilter::ReceiverImpl::ReceiverImpl(const SpdyServerConfig* config,
+ SpdyStream* stream)
+ : config_(config), stream_(stream) {
+ DCHECK(config_);
+ DCHECK(stream_);
+}
+
+HttpToSpdyFilter::ReceiverImpl::~ReceiverImpl() {}
+
+void HttpToSpdyFilter::ReceiverImpl::ReceiveSynReply(
+ net::SpdyHeaderBlock* headers, bool flag_fin) {
+ DCHECK(headers);
+ if (config_->send_version_header()) {
+ (*headers)[http::kXModSpdy] = kModSpdyVersion;
+ }
+ // For client-requested streams, we should send a SYN_REPLY. For
+ // server-pushed streams, the SpdySession has already sent an initial
+ // SYN_STREAM with FLAG_UNIDIRECTIONAL and minimal server push headers, so we
+ // now follow up with a HEADERS frame with the response headers.
+ if (stream_->is_server_push()) {
+ stream_->SendOutputHeaders(*headers, flag_fin);
+ } else {
+ stream_->SendOutputSynReply(*headers, flag_fin);
+ }
+}
+
+void HttpToSpdyFilter::ReceiverImpl::ReceiveData(
+ base::StringPiece data, bool flag_fin) {
+ stream_->SendOutputDataFrame(data, flag_fin);
+}
+
+} // namespace mod_spdy
Added: httpd/mod_spdy/branches/httpd-2.2.x/mod_spdy/apache/filters/http_to_spdy_filter.h
URL: http://svn.apache.org/viewvc/httpd/mod_spdy/branches/httpd-2.2.x/mod_spdy/apache/filters/http_to_spdy_filter.h?rev=1591620&view=auto
==============================================================================
--- httpd/mod_spdy/branches/httpd-2.2.x/mod_spdy/apache/filters/http_to_spdy_filter.h (added)
+++ httpd/mod_spdy/branches/httpd-2.2.x/mod_spdy/apache/filters/http_to_spdy_filter.h Thu May 1 11:39:27 2014
@@ -0,0 +1,81 @@
+// Copyright 2010 Google Inc.
+//
+// Licensed 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.
+
+#ifndef MOD_SPDY_APACHE_FILTERS_HTTP_TO_SPDY_FILTER_H_
+#define MOD_SPDY_APACHE_FILTERS_HTTP_TO_SPDY_FILTER_H_
+
+#include <string>
+
+#include "apr_buckets.h"
+#include "util_filter.h"
+
+#include "base/basictypes.h"
+#include "mod_spdy/common/http_to_spdy_converter.h"
+
+namespace mod_spdy {
+
+class SpdyServerConfig;
+class SpdyStream;
+
+// An Apache filter for converting HTTP data into SPDY frames and sending them
+// to the output queue of a SpdyStream object. This is intended to be the
+// outermost filter in the output chain of one of our slave connections,
+// essentially taking the place of the network socket.
+//
+// In a previous implementation of this filter, we made this a TRANSCODE-level
+// filter rather than a NETWORK-level filter; this had the advantage that we
+// could pull HTTP header data directly from the Apache request object, rather
+// than having to parse the headers. However, it had the disadvantage of being
+// fragile -- for example, we had an additional output filter whose sole job
+// was to deceive Apache into not chunking the response body, and several
+// different hooks to try to make sure our output filters stayed in place even
+// in the face of Apache's weird error-handling paths. Also, using a
+// NETWORK-level filter decreases the likelihood that we'll break other modules
+// that try to use connection-level filters.
+class HttpToSpdyFilter {
+ public:
+ HttpToSpdyFilter(const SpdyServerConfig* config, SpdyStream* stream);
+ ~HttpToSpdyFilter();
+
+ // Read data from the given brigade and write the result through the given
+ // filter. This method is responsible for driving the HTTP to SPDY conversion
+ // process.
+ apr_status_t Write(ap_filter_t* filter, apr_bucket_brigade* input_brigade);
+
+ private:
+ class ReceiverImpl : public HttpToSpdyConverter::SpdyReceiver {
+ public:
+ ReceiverImpl(const SpdyServerConfig* config, SpdyStream* stream);
+ virtual ~ReceiverImpl();
+ virtual void ReceiveSynReply(net::SpdyHeaderBlock* headers, bool flag_fin);
+ virtual void ReceiveData(base::StringPiece data, bool flag_fin);
+
+ private:
+ friend class HttpToSpdyFilter;
+ const SpdyServerConfig* config_;
+ SpdyStream* const stream_;
+
+ DISALLOW_COPY_AND_ASSIGN(ReceiverImpl);
+ };
+
+ ReceiverImpl receiver_;
+ HttpToSpdyConverter converter_;
+ bool eos_bucket_received_;
+
+ DISALLOW_COPY_AND_ASSIGN(HttpToSpdyFilter);
+};
+
+} // namespace mod_spdy
+
+#endif // MOD_SPDY_APACHE_FILTERS_HTTP_TO_SPDY_FILTER_H_
Propchange: httpd/mod_spdy/branches/httpd-2.2.x/mod_spdy/apache/filters/http_to_spdy_filter.h
------------------------------------------------------------------------------
svn:eol-style = native
Added: httpd/mod_spdy/branches/httpd-2.2.x/mod_spdy/apache/filters/http_to_spdy_filter_test.cc
URL: http://svn.apache.org/viewvc/httpd/mod_spdy/branches/httpd-2.2.x/mod_spdy/apache/filters/http_to_spdy_filter_test.cc?rev=1591620&view=auto
==============================================================================
--- httpd/mod_spdy/branches/httpd-2.2.x/mod_spdy/apache/filters/http_to_spdy_filter_test.cc (added)
+++ httpd/mod_spdy/branches/httpd-2.2.x/mod_spdy/apache/filters/http_to_spdy_filter_test.cc Thu May 1 11:39:27 2014
@@ -0,0 +1,527 @@
+// Copyright 2011 Google Inc.
+//
+// Licensed 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 "mod_spdy/apache/filters/http_to_spdy_filter.h"
+
+#include <string>
+
+#include "httpd.h"
+#include "apr_buckets.h"
+#include "apr_tables.h"
+#include "util_filter.h"
+
+#include "base/memory/scoped_ptr.h"
+#include "base/strings/string_piece.h"
+#include "mod_spdy/apache/pool_util.h"
+#include "mod_spdy/common/protocol_util.h"
+#include "mod_spdy/common/shared_flow_control_window.h"
+#include "mod_spdy/common/spdy_frame_priority_queue.h"
+#include "mod_spdy/common/spdy_server_config.h"
+#include "mod_spdy/common/spdy_stream.h"
+#include "mod_spdy/common/testing/spdy_frame_matchers.h"
+#include "mod_spdy/common/version.h"
+#include "net/spdy/spdy_protocol.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using mod_spdy::testing::IsDataFrame;
+using mod_spdy::testing::IsHeaders;
+using mod_spdy::testing::IsSynReply;
+using testing::Pointee;
+
+namespace {
+
+class MockSpdyServerPushInterface : public mod_spdy::SpdyServerPushInterface {
+ public:
+ MOCK_METHOD4(StartServerPush,
+ mod_spdy::SpdyServerPushInterface::PushStatus(
+ net::SpdyStreamId associated_stream_id,
+ int32 server_push_depth,
+ net::SpdyPriority priority,
+ const net::SpdyHeaderBlock& request_headers));
+};
+
+class HttpToSpdyFilterTest :
+ public testing::TestWithParam<mod_spdy::spdy::SpdyVersion> {
+ public:
+ HttpToSpdyFilterTest()
+ : spdy_version_(GetParam()),
+ shared_window_(net::kSpdyStreamInitialWindowSize,
+ net::kSpdyStreamInitialWindowSize),
+ connection_(static_cast<conn_rec*>(
+ apr_pcalloc(local_.pool(), sizeof(conn_rec)))),
+ ap_filter_(static_cast<ap_filter_t*>(
+ apr_pcalloc(local_.pool(), sizeof(ap_filter_t)))),
+ bucket_alloc_(apr_bucket_alloc_create(local_.pool())),
+ brigade_(apr_brigade_create(local_.pool(), bucket_alloc_)) {
+ // Set up our Apache data structures. To keep things simple, we set only
+ // the bare minimum of necessary fields, and rely on apr_pcalloc to zero
+ // all others.
+ connection_->pool = local_.pool();
+ ap_filter_->c = connection_;
+ }
+
+ protected:
+ void AddHeapBucket(base::StringPiece str) {
+ APR_BRIGADE_INSERT_TAIL(brigade_, apr_bucket_heap_create(
+ str.data(), str.size(), NULL, bucket_alloc_));
+ }
+
+ void AddImmortalBucket(base::StringPiece str) {
+ APR_BRIGADE_INSERT_TAIL(brigade_, apr_bucket_immortal_create(
+ str.data(), str.size(), bucket_alloc_));
+ }
+
+ void AddFlushBucket() {
+ APR_BRIGADE_INSERT_TAIL(brigade_, apr_bucket_flush_create(bucket_alloc_));
+ }
+
+ void AddEosBucket() {
+ APR_BRIGADE_INSERT_TAIL(brigade_, apr_bucket_eos_create(bucket_alloc_));
+ }
+
+ apr_status_t WriteBrigade(mod_spdy::HttpToSpdyFilter* filter) {
+ return filter->Write(ap_filter_, brigade_);
+ }
+
+ void ExpectSynReply(net::SpdyStreamId stream_id,
+ const net::SpdyHeaderBlock& headers,
+ bool flag_fin) {
+ net::SpdyFrameIR* raw_frame = NULL;
+ ASSERT_TRUE(output_queue_.Pop(&raw_frame));
+ ASSERT_TRUE(raw_frame != NULL);
+ scoped_ptr<net::SpdyFrameIR> frame(raw_frame);
+ EXPECT_THAT(*frame, IsSynReply(stream_id, flag_fin, headers));
+ }
+
+ void ExpectHeaders(net::SpdyStreamId stream_id,
+ const net::SpdyHeaderBlock& headers,
+ bool flag_fin) {
+ net::SpdyFrameIR* raw_frame = NULL;
+ ASSERT_TRUE(output_queue_.Pop(&raw_frame));
+ ASSERT_TRUE(raw_frame != NULL);
+ scoped_ptr<net::SpdyFrameIR> frame(raw_frame);
+ EXPECT_THAT(*frame, IsHeaders(stream_id, flag_fin, headers));
+ }
+
+ void ExpectDataFrame(net::SpdyStreamId stream_id, base::StringPiece data,
+ bool flag_fin) {
+ net::SpdyFrameIR* raw_frame = NULL;
+ ASSERT_TRUE(output_queue_.Pop(&raw_frame));
+ ASSERT_TRUE(raw_frame != NULL);
+ scoped_ptr<net::SpdyFrameIR> frame(raw_frame);
+ EXPECT_THAT(*frame, IsDataFrame(stream_id, flag_fin, data));
+ }
+
+ void ExpectOutputQueueEmpty() {
+ net::SpdyFrameIR* frame;
+ EXPECT_FALSE(output_queue_.Pop(&frame));
+ }
+
+ const char* status_header_name() const {
+ return (spdy_version_ < mod_spdy::spdy::SPDY_VERSION_3 ?
+ mod_spdy::spdy::kSpdy2Status : mod_spdy::spdy::kSpdy3Status);
+ }
+
+ const char* version_header_name() const {
+ return (spdy_version_ < mod_spdy::spdy::SPDY_VERSION_3 ?
+ mod_spdy::spdy::kSpdy2Version : mod_spdy::spdy::kSpdy3Version);
+ }
+
+ const mod_spdy::spdy::SpdyVersion spdy_version_;
+ mod_spdy::SpdyFramePriorityQueue output_queue_;
+ mod_spdy::SharedFlowControlWindow shared_window_;
+ MockSpdyServerPushInterface pusher_;
+ mod_spdy::LocalPool local_;
+ conn_rec* const connection_;
+ ap_filter_t* const ap_filter_;
+ apr_bucket_alloc_t* const bucket_alloc_;
+ apr_bucket_brigade* const brigade_;
+};
+
+TEST_P(HttpToSpdyFilterTest, ResponseWithContentLength) {
+ // Set up our data structures that we're testing:
+ const net::SpdyStreamId stream_id = 3;
+ const net::SpdyStreamId associated_stream_id = 0;
+ const int32 initial_server_push_depth = 0;
+ const net::SpdyPriority priority = 0;
+ mod_spdy::SpdyStream stream(
+ spdy_version_, stream_id, associated_stream_id,
+ initial_server_push_depth, priority, net::kSpdyStreamInitialWindowSize,
+ &output_queue_, &shared_window_, &pusher_);
+ mod_spdy::SpdyServerConfig config;
+ mod_spdy::HttpToSpdyFilter http_to_spdy_filter(&config, &stream);
+
+ // Send part of the header data into the filter:
+ AddImmortalBucket("HTTP/1.1 200 OK\r\n"
+ "Connection: close\r\n");
+ ASSERT_EQ(APR_SUCCESS, WriteBrigade(&http_to_spdy_filter));
+ EXPECT_TRUE(APR_BRIGADE_EMPTY(brigade_));
+
+ // Expect to get nothing out yet:
+ ExpectOutputQueueEmpty();
+
+ // Send the rest of the header data into the filter:
+ AddImmortalBucket("Content-Length: 12000\r\n"
+ "Content-Type: text/html\r\n"
+ "Host: www.example.com\r\n"
+ "\r\n");
+ ASSERT_EQ(APR_SUCCESS, WriteBrigade(&http_to_spdy_filter));
+ EXPECT_TRUE(APR_BRIGADE_EMPTY(brigade_));
+
+ // Expect to get a single SYN_REPLY frame out with all the headers:
+ net::SpdyHeaderBlock expected_headers;
+ expected_headers[mod_spdy::http::kContentLength] = "12000";
+ expected_headers[mod_spdy::http::kContentType] = "text/html";
+ expected_headers[mod_spdy::http::kHost] = "www.example.com";
+ expected_headers[status_header_name()] = "200";
+ expected_headers[version_header_name()] = "HTTP/1.1";
+ expected_headers[mod_spdy::http::kXModSpdy] =
+ MOD_SPDY_VERSION_STRING "-" LASTCHANGE_STRING;
+ ExpectSynReply(stream_id, expected_headers, false);
+ ExpectOutputQueueEmpty();
+
+ // Now send in some body data, with a FLUSH bucket:
+ AddHeapBucket(std::string(1000, 'a'));
+ AddFlushBucket();
+ ASSERT_EQ(APR_SUCCESS, WriteBrigade(&http_to_spdy_filter));
+ EXPECT_TRUE(APR_BRIGADE_EMPTY(brigade_));
+
+ // Expect to get a single data frame out, containing the data we just sent:
+ ExpectDataFrame(stream_id, std::string(1000, 'a'), false);
+ ExpectOutputQueueEmpty();
+
+ // Send in some more body data, this time with no FLUSH bucket:
+ AddHeapBucket(std::string(2000, 'b'));
+ ASSERT_EQ(APR_SUCCESS, WriteBrigade(&http_to_spdy_filter));
+ EXPECT_TRUE(APR_BRIGADE_EMPTY(brigade_));
+
+ // Expect to get nothing more out yet (because there's too little data to be
+ // worth sending a frame):
+ ExpectOutputQueueEmpty();
+
+ // Send lots more body data, again with a FLUSH bucket:
+ AddHeapBucket(std::string(3000, 'c'));
+ AddFlushBucket();
+ ASSERT_EQ(APR_SUCCESS, WriteBrigade(&http_to_spdy_filter));
+ EXPECT_TRUE(APR_BRIGADE_EMPTY(brigade_));
+
+ // This time, we should get two data frames out.
+ ExpectDataFrame(stream_id, std::string(2000, 'b') + std::string(2096, 'c'),
+ false);
+ ExpectDataFrame(stream_id, std::string(904, 'c'), false);
+ ExpectOutputQueueEmpty();
+
+ // Finally, send a bunch more data, followed by an EOS bucket:
+ AddHeapBucket(std::string(6000, 'd'));
+ AddEosBucket();
+ ASSERT_EQ(APR_SUCCESS, WriteBrigade(&http_to_spdy_filter));
+ EXPECT_TRUE(APR_BRIGADE_EMPTY(brigade_));
+
+ // We should get two last data frames, the latter having FLAG_FIN set.
+ ExpectDataFrame(stream_id, std::string(4096, 'd'), false);
+ ExpectDataFrame(stream_id, std::string(1904, 'd'), true);
+ ExpectOutputQueueEmpty();
+}
+
+TEST_P(HttpToSpdyFilterTest, ChunkedResponse) {
+ // Set up our data structures that we're testing:
+ const net::SpdyStreamId stream_id = 3;
+ const net::SpdyStreamId associated_stream_id = 0;
+ const int32 initial_server_push_depth = 0;
+ const net::SpdyPriority priority = 0;
+ mod_spdy::SpdyStream stream(
+ spdy_version_, stream_id, associated_stream_id,
+ initial_server_push_depth, priority, net::kSpdyStreamInitialWindowSize,
+ &output_queue_, &shared_window_, &pusher_);
+ mod_spdy::SpdyServerConfig config;
+ mod_spdy::HttpToSpdyFilter http_to_spdy_filter(&config, &stream);
+
+ // Send part of the header data into the filter:
+ AddImmortalBucket("HTTP/1.1 200 OK\r\n"
+ "Keep-Alive: timeout=120\r\n");
+ ASSERT_EQ(APR_SUCCESS, WriteBrigade(&http_to_spdy_filter));
+ EXPECT_TRUE(APR_BRIGADE_EMPTY(brigade_));
+
+ // Expect to get nothing out yet:
+ ExpectOutputQueueEmpty();
+
+ // Send the rest of the header data into the filter:
+ AddImmortalBucket("Content-Type: text/html\r\n"
+ "Transfer-Encoding: chunked\r\n"
+ "Host: www.example.com\r\n"
+ "\r\n");
+ ASSERT_EQ(APR_SUCCESS, WriteBrigade(&http_to_spdy_filter));
+ EXPECT_TRUE(APR_BRIGADE_EMPTY(brigade_));
+
+ // Expect to get a single SYN_REPLY frame out with all the headers:
+ net::SpdyHeaderBlock expected_headers;
+ expected_headers[mod_spdy::http::kContentType] = "text/html";
+ expected_headers[mod_spdy::http::kHost] = "www.example.com";
+ expected_headers[status_header_name()] = "200";
+ expected_headers[version_header_name()] = "HTTP/1.1";
+ expected_headers[mod_spdy::http::kXModSpdy] =
+ MOD_SPDY_VERSION_STRING "-" LASTCHANGE_STRING;
+ ExpectSynReply(stream_id, expected_headers, false);
+ ExpectOutputQueueEmpty();
+
+ // Now send in some body data, with a FLUSH bucket:
+ AddImmortalBucket("1B\r\n");
+ AddImmortalBucket("abcdefghijklmnopqrstuvwxyz\n\r\n");
+ AddImmortalBucket("17\r\n");
+ AddImmortalBucket("That was ");
+ AddFlushBucket();
+ ASSERT_EQ(APR_SUCCESS, WriteBrigade(&http_to_spdy_filter));
+ EXPECT_TRUE(APR_BRIGADE_EMPTY(brigade_));
+
+ // Expect to get a single data frame out, containing the data we just sent:
+ ExpectDataFrame(stream_id, "abcdefghijklmnopqrstuvwxyz\nThat was ", false);
+ ExpectOutputQueueEmpty();
+
+ // Send in some more body data, this time with no FLUSH bucket:
+ AddImmortalBucket("the alphabet.\n\r\n");
+ ASSERT_EQ(APR_SUCCESS, WriteBrigade(&http_to_spdy_filter));
+ EXPECT_TRUE(APR_BRIGADE_EMPTY(brigade_));
+
+ // Expect to get nothing more out yet (because there's too little data to be
+ // worth sending a frame):
+ ExpectOutputQueueEmpty();
+
+ // Finally, terminate the response:
+ AddImmortalBucket("0\r\n\r\n");
+ AddEosBucket();
+ ASSERT_EQ(APR_SUCCESS, WriteBrigade(&http_to_spdy_filter));
+ EXPECT_TRUE(APR_BRIGADE_EMPTY(brigade_));
+
+ // We should get the last data frame, with FLAG_FIN set.
+ ExpectDataFrame(stream_id, "the alphabet.\n", true);
+ ExpectOutputQueueEmpty();
+}
+
+TEST_P(HttpToSpdyFilterTest, RedirectResponse) {
+ // Set up our data structures that we're testing:
+ const net::SpdyStreamId stream_id = 5;
+ const net::SpdyStreamId associated_stream_id = 0;
+ const int32 initial_server_push_depth = 0;
+ const net::SpdyPriority priority = 0;
+ mod_spdy::SpdyStream stream(
+ spdy_version_, stream_id, associated_stream_id,
+ initial_server_push_depth, priority, net::kSpdyStreamInitialWindowSize,
+ &output_queue_, &shared_window_, &pusher_);
+ mod_spdy::SpdyServerConfig config;
+ mod_spdy::HttpToSpdyFilter http_to_spdy_filter(&config, &stream);
+
+ // Send part of the header data into the filter:
+ AddImmortalBucket("HTTP/1.1 301 Moved Permanently\r\n"
+ "Location: http://www.example.net/\r\n");
+ ASSERT_EQ(APR_SUCCESS, WriteBrigade(&http_to_spdy_filter));
+ EXPECT_TRUE(APR_BRIGADE_EMPTY(brigade_));
+
+ // Expect to get nothing out yet:
+ ExpectOutputQueueEmpty();
+
+ // Signal the end of the leading headers:
+ AddImmortalBucket("\r\n");
+ ASSERT_EQ(APR_SUCCESS, WriteBrigade(&http_to_spdy_filter));
+ EXPECT_TRUE(APR_BRIGADE_EMPTY(brigade_));
+
+ // Expect to get a single SYN_REPLY frame out. This response has no body, so
+ // FLAG_FIN should be set.
+ net::SpdyHeaderBlock expected_headers;
+ expected_headers["location"] = "http://www.example.net/";
+ expected_headers[status_header_name()] = "301";
+ expected_headers[version_header_name()] = "HTTP/1.1";
+ expected_headers[mod_spdy::http::kXModSpdy] =
+ MOD_SPDY_VERSION_STRING "-" LASTCHANGE_STRING;
+ ExpectSynReply(stream_id, expected_headers, true);
+ ExpectOutputQueueEmpty();
+}
+
+// Test that the filter accepts empty brigades.
+TEST_P(HttpToSpdyFilterTest, AcceptEmptyBrigade) {
+ // Set up our data structures that we're testing:
+ const net::SpdyStreamId stream_id = 5;
+ const net::SpdyStreamId associated_stream_id = 0;
+ const int32 initial_server_push_depth = 0;
+ const net::SpdyPriority priority = 0;
+ mod_spdy::SpdyStream stream(
+ spdy_version_, stream_id, associated_stream_id,
+ initial_server_push_depth, priority, net::kSpdyStreamInitialWindowSize,
+ &output_queue_, &shared_window_, &pusher_);
+ mod_spdy::SpdyServerConfig config;
+ mod_spdy::HttpToSpdyFilter http_to_spdy_filter(&config, &stream);
+
+ // Send the header data into the filter:
+ AddImmortalBucket("HTTP/1.1 200 OK\r\n"
+ "Content-Length: 6\r\n"
+ "Content-Type: text/plain\r\n"
+ "\r\n");
+ ASSERT_EQ(APR_SUCCESS, WriteBrigade(&http_to_spdy_filter));
+ net::SpdyHeaderBlock expected_headers;
+ expected_headers[mod_spdy::http::kContentLength] = "6";
+ expected_headers[mod_spdy::http::kContentType] = "text/plain";
+ expected_headers[status_header_name()] = "200";
+ expected_headers[version_header_name()] = "HTTP/1.1";
+ expected_headers[mod_spdy::http::kXModSpdy] =
+ MOD_SPDY_VERSION_STRING "-" LASTCHANGE_STRING;
+ ExpectSynReply(stream_id, expected_headers, false);
+ ExpectOutputQueueEmpty();
+
+ // Send in some body data:
+ AddImmortalBucket("foo");
+ ASSERT_EQ(APR_SUCCESS, WriteBrigade(&http_to_spdy_filter));
+ EXPECT_TRUE(APR_BRIGADE_EMPTY(brigade_));
+ ExpectOutputQueueEmpty();
+
+ // Run the filter again, with an empty brigade. It should accept it and do
+ // nothing.
+ ASSERT_EQ(APR_SUCCESS, WriteBrigade(&http_to_spdy_filter));
+ EXPECT_TRUE(APR_BRIGADE_EMPTY(brigade_));
+ ExpectOutputQueueEmpty();
+
+ // Send in the rest of the body data.
+ AddImmortalBucket("bar");
+ AddEosBucket();
+ ASSERT_EQ(APR_SUCCESS, WriteBrigade(&http_to_spdy_filter));
+ EXPECT_TRUE(APR_BRIGADE_EMPTY(brigade_));
+ ExpectDataFrame(stream_id, "foobar", true);
+ ExpectOutputQueueEmpty();
+}
+
+// Test that the filter behaves correctly when a stream is aborted halfway
+// through producing output.
+TEST_P(HttpToSpdyFilterTest, StreamAbort) {
+ // Set up our data structures that we're testing:
+ const net::SpdyStreamId stream_id = 7;
+ const net::SpdyStreamId associated_stream_id = 0;
+ const int32 initial_server_push_depth = 0;
+ const net::SpdyPriority priority =
+ mod_spdy::LowestSpdyPriorityForVersion(spdy_version_);
+ mod_spdy::SpdyStream stream(
+ spdy_version_, stream_id, associated_stream_id,
+ initial_server_push_depth, priority, net::kSpdyStreamInitialWindowSize,
+ &output_queue_, &shared_window_, &pusher_);
+ mod_spdy::SpdyServerConfig config;
+ mod_spdy::HttpToSpdyFilter http_to_spdy_filter(&config, &stream);
+
+ // Send the header data into the filter:
+ AddImmortalBucket("HTTP/1.1 200 OK\r\n"
+ "Content-Length: 6\r\n"
+ "Content-Type: text/plain\r\n"
+ "\r\n");
+ ASSERT_EQ(APR_SUCCESS, WriteBrigade(&http_to_spdy_filter));
+ net::SpdyHeaderBlock expected_headers;
+ expected_headers[mod_spdy::http::kContentLength] = "6";
+ expected_headers[mod_spdy::http::kContentType] = "text/plain";
+ expected_headers[status_header_name()] = "200";
+ expected_headers[version_header_name()] = "HTTP/1.1";
+ expected_headers[mod_spdy::http::kXModSpdy] =
+ MOD_SPDY_VERSION_STRING "-" LASTCHANGE_STRING;
+ ExpectSynReply(stream_id, expected_headers, false);
+ ExpectOutputQueueEmpty();
+
+ // Send in some body data:
+ AddImmortalBucket("foo");
+ ASSERT_EQ(APR_SUCCESS, WriteBrigade(&http_to_spdy_filter));
+ EXPECT_TRUE(APR_BRIGADE_EMPTY(brigade_));
+ ExpectOutputQueueEmpty();
+
+ // Abort the stream, and then try to send in more data. We should get back
+ // ECONNABORTED, the brigade should remain unconsumed, and the connection
+ // should be marked as aborted.
+ stream.AbortSilently();
+ AddImmortalBucket("bar");
+ AddEosBucket();
+ ASSERT_FALSE(connection_->aborted);
+ ASSERT_TRUE(APR_STATUS_IS_ECONNABORTED(WriteBrigade(&http_to_spdy_filter)));
+ EXPECT_FALSE(APR_BRIGADE_EMPTY(brigade_));
+ ExpectOutputQueueEmpty();
+ ASSERT_TRUE(connection_->aborted);
+}
+
+TEST_P(HttpToSpdyFilterTest, ServerPushedStream) {
+ // Set up our data structures that we're testing:
+ const net::SpdyStreamId stream_id = 4;
+ const net::SpdyStreamId associated_stream_id = 3;
+ const int32 initial_server_push_depth = 0;
+ const net::SpdyPriority priority = 0;
+ mod_spdy::SpdyStream stream(
+ spdy_version_, stream_id, associated_stream_id,
+ initial_server_push_depth, priority, net::kSpdyStreamInitialWindowSize,
+ &output_queue_, &shared_window_, &pusher_);
+ mod_spdy::SpdyServerConfig config;
+ mod_spdy::HttpToSpdyFilter http_to_spdy_filter(&config, &stream);
+
+ // Send the response data into the filter:
+ AddImmortalBucket("HTTP/1.1 200 OK\r\n"
+ "Content-Length: 20\r\n"
+ "Content-Type: text/css\r\n"
+ "\r\n"
+ "BODY { color: red; }");
+ ASSERT_EQ(APR_SUCCESS, WriteBrigade(&http_to_spdy_filter));
+ EXPECT_TRUE(APR_BRIGADE_EMPTY(brigade_));
+
+ // Since this is a server push stream, the SpdySession should have earlier
+ // sent a SYN_STREAM with FLAG_UNIDIRECTIONAL, and we now expect to see a
+ // HEADERS frame from the filter (rather than a SYN_REPLY):
+ net::SpdyHeaderBlock expected_headers;
+ expected_headers[mod_spdy::http::kContentLength] = "20";
+ expected_headers[mod_spdy::http::kContentType] = "text/css";
+ expected_headers[status_header_name()] = "200";
+ expected_headers[version_header_name()] = "HTTP/1.1";
+ expected_headers[mod_spdy::http::kXModSpdy] =
+ MOD_SPDY_VERSION_STRING "-" LASTCHANGE_STRING;
+ ExpectHeaders(stream_id, expected_headers, false);
+ // And also the pushed data:
+ ExpectDataFrame(stream_id, "BODY { color: red; }", true);
+ ExpectOutputQueueEmpty();
+}
+
+TEST_P(HttpToSpdyFilterTest, DoNotSendVersionHeaderWhenAskedNotTo) {
+ // Set up our data structures that we're testing:
+ const net::SpdyStreamId stream_id = 5;
+ const net::SpdyStreamId associated_stream_id = 0;
+ const int32 initial_server_push_depth = 0;
+ const net::SpdyPriority priority = 0;
+ mod_spdy::SpdyStream stream(
+ spdy_version_, stream_id, associated_stream_id,
+ initial_server_push_depth, priority, net::kSpdyStreamInitialWindowSize,
+ &output_queue_, &shared_window_, &pusher_);
+ mod_spdy::SpdyServerConfig config;
+ config.set_send_version_header(false);
+ mod_spdy::HttpToSpdyFilter http_to_spdy_filter(&config, &stream);
+
+ // Send the response into the filter:
+ AddImmortalBucket("HTTP/1.1 301 Moved Permanently\r\n"
+ "Location: http://www.example.net/\r\n"
+ "\r\n");
+ ASSERT_EQ(APR_SUCCESS, WriteBrigade(&http_to_spdy_filter));
+ EXPECT_TRUE(APR_BRIGADE_EMPTY(brigade_));
+
+ // Expect to get a single SYN_REPLY frame out. This response has no body, so
+ // FLAG_FIN should be set. There should be no version header.
+ net::SpdyHeaderBlock expected_headers;
+ expected_headers["location"] = "http://www.example.net/";
+ expected_headers[status_header_name()] = "301";
+ expected_headers[version_header_name()] = "HTTP/1.1";
+ ExpectSynReply(stream_id, expected_headers, true);
+ ExpectOutputQueueEmpty();
+}
+
+// Run each test over SPDY/2, SPDY/3, and SPDY/3.1.
+INSTANTIATE_TEST_CASE_P(Spdy2And3, HttpToSpdyFilterTest, testing::Values(
+ mod_spdy::spdy::SPDY_VERSION_2, mod_spdy::spdy::SPDY_VERSION_3,
+ mod_spdy::spdy::SPDY_VERSION_3_1));
+
+} // namespace
Added: httpd/mod_spdy/branches/httpd-2.2.x/mod_spdy/apache/filters/server_push_discovery_filter.cc
URL: http://svn.apache.org/viewvc/httpd/mod_spdy/branches/httpd-2.2.x/mod_spdy/apache/filters/server_push_discovery_filter.cc?rev=1591620&view=auto
==============================================================================
--- httpd/mod_spdy/branches/httpd-2.2.x/mod_spdy/apache/filters/server_push_discovery_filter.cc (added)
+++ httpd/mod_spdy/branches/httpd-2.2.x/mod_spdy/apache/filters/server_push_discovery_filter.cc Thu May 1 11:39:27 2014
@@ -0,0 +1,124 @@
+// Copyright 2014 Google Inc.
+//
+// Licensed 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 "mod_spdy/apache/filters/server_push_discovery_filter.h"
+
+#include <string>
+
+#include "apr.h"
+#include "apr_strings.h"
+#include "base/logging.h"
+#include "base/safe_numerics.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/stringprintf.h"
+#include "mod_spdy/apache/slave_connection_context.h"
+#include "mod_spdy/common/protocol_util.h"
+#include "mod_spdy/common/server_push_discovery_learner.h"
+#include "mod_spdy/common/server_push_discovery_session.h"
+#include "util_cookies.h"
+
+namespace mod_spdy {
+
+namespace {
+
+const char kServerPushDiscoveryCookieName[] = "ServerPushDiscoverySession";
+
+}
+
+void ServerPushDiscoveryFilter(
+ ap_filter_t* filter,
+ apr_bucket_brigade* input_brigade,
+ ServerPushDiscoveryLearner* learner,
+ ServerPushDiscoverySessionPool* session_pool,
+ int spdy_version,
+ bool send_debug_headers) {
+ const char kServerPushDiscoveryCookieAttr[] = "Discard;Version=1";
+ long kServerPushDiscoveryCookieMaxAge = 10; // 10 seconds.
+
+ // Exclude requests that already have the x-associated-content header set.
+ if (apr_table_get(filter->r->headers_out, http::kXAssociatedContent)) {
+ return;
+ }
+
+ apr_time_t request_time = filter->r->request_time;
+
+ // Get server push discovery session from cookie.
+ apr_int64_t session_id = 0;
+ const char* cookie_val = NULL;
+ apr_status_t status = util_cookies::ap_cookie_read(
+ filter->r, kServerPushDiscoveryCookieName, &cookie_val, true);
+ bool existing_session = false;
+ if (APR_SUCCESS == status && cookie_val) {
+ existing_session = base::StringToInt64(cookie_val, &session_id);
+ }
+
+ // If we have a pre-existing session, learn adjacent hits only.
+ if (existing_session) {
+ ServerPushDiscoverySession* session =
+ session_pool->GetExistingSession(session_id, request_time);
+
+ if (session && session->master_url() != std::string(filter->r->uri)) {
+ session->UpdateLastAccessTime(request_time);
+
+ if (!session->took_push()) {
+ learner->AddAdjacentHit(session->master_url(), filter->r->uri,
+ session->TimeFromInit(request_time));
+ }
+
+ return;
+ }
+ }
+
+ // Requests without existing sessions are treated as initial requests.
+ bool takes_push = spdy_version >= spdy::SPDY_VERSION_3;
+
+ // Set-up a push discovery session for the client using a short-lived cookie.
+ int64_t new_session_id =
+ session_pool->CreateSession(request_time, filter->r->uri, takes_push);
+ apr_table_add(filter->r->headers_out, "Cache-Control",
+ "no-cache=\"set-cookie\"");
+ util_cookies::ap_cookie_write(filter->r, kServerPushDiscoveryCookieName,
+ base::Int64ToString(new_session_id).c_str(),
+ kServerPushDiscoveryCookieAttr,
+ kServerPushDiscoveryCookieMaxAge,
+ filter->r->headers_out,
+ NULL);
+
+ // Write push instructions for client that can use it. Otherwise log an
+ // initial hit.
+ if (takes_push) {
+ std::vector<ServerPushDiscoveryLearner::Push> pushes =
+ learner->GetPushes(filter->r->uri);
+
+ std::string push_content;
+ for (std::vector<ServerPushDiscoveryLearner::Push>::iterator it =
+ pushes.begin();
+ it != pushes.end(); ++it) {
+ base::StringAppendF(&push_content, "\"%s\":%d,", it->adjacent_url.c_str(),
+ base::checked_numeric_cast<int>(it->priority));
+ }
+
+ apr_table_add(filter->r->headers_out, http::kXAssociatedContent,
+ push_content.c_str());
+
+ if (send_debug_headers) {
+ apr_table_add(filter->r->headers_out, "x-associated-content-debug",
+ push_content.c_str());
+ }
+ } else {
+ learner->AddFirstHit(filter->r->uri);
+ }
+}
+
+} // namespace mod_spdy
Added: httpd/mod_spdy/branches/httpd-2.2.x/mod_spdy/apache/filters/server_push_discovery_filter.h
URL: http://svn.apache.org/viewvc/httpd/mod_spdy/branches/httpd-2.2.x/mod_spdy/apache/filters/server_push_discovery_filter.h?rev=1591620&view=auto
==============================================================================
--- httpd/mod_spdy/branches/httpd-2.2.x/mod_spdy/apache/filters/server_push_discovery_filter.h (added)
+++ httpd/mod_spdy/branches/httpd-2.2.x/mod_spdy/apache/filters/server_push_discovery_filter.h Thu May 1 11:39:27 2014
@@ -0,0 +1,41 @@
+// Copyright 2014 Google Inc.
+//
+// Licensed 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.
+
+#ifndef MOD_SPDY_APACHE_FILTERS_SERVER_PUSH_DISCOVERY_FILTER_H_
+#define MOD_SPDY_APACHE_FILTERS_SERVER_PUSH_DISCOVERY_FILTER_H_
+
+#include <string>
+
+#include "apr_buckets.h"
+#include "util_filter.h"
+
+#include "mod_spdy/common/spdy_server_config.h"
+
+namespace mod_spdy {
+
+class ServerPushDiscoveryLearner;
+class ServerPushDiscoverySessionPool;
+
+void ServerPushDiscoveryFilter(
+ ap_filter_t* filter,
+ apr_bucket_brigade* input_brigade,
+ ServerPushDiscoveryLearner* learner,
+ ServerPushDiscoverySessionPool* session_pool,
+ int spdy_version,
+ bool send_debug_headers);
+
+
+} // namespace mod_spdy
+
+#endif // MOD_SPDY_APACHE_FILTERS_SERVER_PUSH_FILTER_H_
Propchange: httpd/mod_spdy/branches/httpd-2.2.x/mod_spdy/apache/filters/server_push_discovery_filter.h
------------------------------------------------------------------------------
svn:eol-style = native
Added: httpd/mod_spdy/branches/httpd-2.2.x/mod_spdy/apache/filters/server_push_discovery_filter_test.cc
URL: http://svn.apache.org/viewvc/httpd/mod_spdy/branches/httpd-2.2.x/mod_spdy/apache/filters/server_push_discovery_filter_test.cc?rev=1591620&view=auto
==============================================================================
--- httpd/mod_spdy/branches/httpd-2.2.x/mod_spdy/apache/filters/server_push_discovery_filter_test.cc (added)
+++ httpd/mod_spdy/branches/httpd-2.2.x/mod_spdy/apache/filters/server_push_discovery_filter_test.cc Thu May 1 11:39:27 2014
@@ -0,0 +1,167 @@
+// Copyright 2014 Google Inc.
+//
+// Licensed 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 "mod_spdy/apache/filters/server_push_discovery_filter.h"
+
+#include <string>
+
+#include "httpd.h"
+#include "apr_buckets.h"
+#include "apr_strings.h"
+#include "apr_tables.h"
+#include "util_filter.h"
+
+#include "base/strings/string_piece.h"
+#include "base/strings/stringprintf.h"
+#include "mod_spdy/apache/pool_util.h"
+#include "mod_spdy/common/protocol_util.h"
+#include "mod_spdy/common/server_push_discovery_learner.h"
+#include "mod_spdy/common/server_push_discovery_session.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+
+const char* const kRefererUrl = "https://www.example.com/index.html";
+
+class ServerPushDiscoveryFilterTest : public testing::Test {
+ public:
+ ServerPushDiscoveryFilterTest()
+ : bucket_alloc_(apr_bucket_alloc_create(local_.pool())),
+ brigade_(apr_brigade_create(local_.pool(), bucket_alloc_)) {
+ }
+
+ virtual void SetUp() {
+ RunThroughFilter("index.html", 0, -1, mod_spdy::spdy::SPDY_VERSION_NONE);
+ RunThroughFilter("a.html", 10, 0, mod_spdy::spdy::SPDY_VERSION_NONE);
+ RunThroughFilter("b.html", 20, 0, mod_spdy::spdy::SPDY_VERSION_NONE);
+ }
+
+ ap_filter_t* RunThroughFilter(
+ const std::string& uri, apr_time_t request_time, int session_id,
+ mod_spdy::spdy::SpdyVersion spdy_version) {
+ ap_filter_t* ap_filter = MakeFilterChain(uri, request_time, session_id);
+ mod_spdy::ServerPushDiscoveryFilter(ap_filter, brigade_, &learner_,
+ &session_pool_, spdy_version, true);
+ return ap_filter;
+ }
+
+ // No session cookie is written when |session_id| is negative.
+ ap_filter_t* MakeFilterChain(
+ const std::string& uri, apr_time_t request_time, int session_id) {
+ conn_rec* const connection(static_cast<conn_rec*>(
+ apr_pcalloc(local_.pool(), sizeof(conn_rec))));
+ request_rec* const request(static_cast<request_rec*>(
+ apr_pcalloc(local_.pool(), sizeof(request_rec))));
+ ap_filter_t* const ap_filter(static_cast<ap_filter_t*>(
+ apr_pcalloc(local_.pool(), sizeof(ap_filter_t))));
+
+ // Set up our Apache data structures. To keep things simple, we set only
+ // the bare minimum of necessary fields, and rely on apr_pcalloc to zero
+ // all others.
+ connection->pool = local_.pool();
+ request->pool = local_.pool();
+ request->connection = connection;
+ request->request_time = request_time;
+ request->headers_in = apr_table_make(local_.pool(), 5);
+ request->headers_out = apr_table_make(local_.pool(), 5);
+ request->err_headers_out = apr_table_make(local_.pool(), 5);
+ request->protocol = const_cast<char*>("HTTP/1.1");
+ request->uri = apr_pstrdup(local_.pool(), uri.c_str());
+ ap_filter->c = connection;
+ ap_filter->r = request;
+
+ if (session_id >= 0) {
+ std::string header_value =
+ base::StringPrintf("%s=%d", "ServerPushDiscoverySession", session_id);
+ apr_table_set(request->headers_in, "Cookie", header_value.c_str());
+ }
+
+ apr_table_setn(request->headers_in, mod_spdy::http::kHost,
+ "www.example.com");
+
+ return ap_filter;
+ }
+
+ std::string GetXACHeader(ap_filter_t* ap_filter) {
+ const char* value = apr_table_get(ap_filter->r->headers_out,
+ mod_spdy::http::kXAssociatedContent);
+ if (value) {
+ return std::string(value);
+ } else {
+ return std::string();
+ }
+ }
+
+ protected:
+ mod_spdy::LocalPool local_;
+ mod_spdy::ServerPushDiscoveryLearner learner_;
+ mod_spdy::ServerPushDiscoverySessionPool session_pool_;
+ apr_bucket_alloc_t* const bucket_alloc_;
+ apr_bucket_brigade* const brigade_;
+};
+
+TEST_F(ServerPushDiscoveryFilterTest, UntrainedNoEffect) {
+ ap_filter_t* const ap_filter(RunThroughFilter(
+ "untrained.html", 0, -1, mod_spdy::spdy::SPDY_VERSION_3_1));
+ EXPECT_EQ("", GetXACHeader(ap_filter));
+}
+
+TEST_F(ServerPushDiscoveryFilterTest, GenerateHeadersForSPDY3) {
+ ap_filter_t* const ap_filter(RunThroughFilter(
+ "index.html", 50, -1, mod_spdy::spdy::SPDY_VERSION_3_1));
+ EXPECT_EQ("\"a.html\":2,\"b.html\":5,", GetXACHeader(ap_filter));
+}
+
+TEST_F(ServerPushDiscoveryFilterTest, NoHeadersForSPDY2) {
+ ap_filter_t* const ap_filter(RunThroughFilter(
+ "index.html", 50, -1, mod_spdy::spdy::SPDY_VERSION_2));
+ EXPECT_EQ("", GetXACHeader(ap_filter));
+}
+
+TEST_F(ServerPushDiscoveryFilterTest, NoHeadersForHTTPS) {
+ ap_filter_t* const ap_filter(RunThroughFilter(
+ "index.html", 50, -1, mod_spdy::spdy::SPDY_VERSION_NONE));
+ EXPECT_EQ("", GetXACHeader(ap_filter));
+}
+
+TEST_F(ServerPushDiscoveryFilterTest, PassThroughExistingXACHeaders) {
+ ap_filter_t* const ap_filter(MakeFilterChain("index.html", 80, -1));
+ apr_table_add(ap_filter->r->headers_out,
+ mod_spdy::http::kXAssociatedContent, "TestXACValue");
+ mod_spdy::ServerPushDiscoveryFilter(ap_filter, brigade_,
+ &learner_, &session_pool_,
+ mod_spdy::spdy::SPDY_VERSION_3_1, true);
+ EXPECT_EQ("TestXACValue", GetXACHeader(ap_filter));
+}
+
+TEST_F(ServerPushDiscoveryFilterTest, AdditionalTraining) {
+ RunThroughFilter("index.html", 100, -1, mod_spdy::spdy::SPDY_VERSION_NONE);
+ RunThroughFilter("b.html", 110, 1, mod_spdy::spdy::SPDY_VERSION_NONE);
+ RunThroughFilter("c.html", 120, 1, mod_spdy::spdy::SPDY_VERSION_NONE);
+
+ RunThroughFilter("index.html", 200, -1, mod_spdy::spdy::SPDY_VERSION_NONE);
+ RunThroughFilter("b.html", 210, 2, mod_spdy::spdy::SPDY_VERSION_NONE);
+ RunThroughFilter("c.html", 220, 2, mod_spdy::spdy::SPDY_VERSION_NONE);
+
+ RunThroughFilter("index.html", 300, -1, mod_spdy::spdy::SPDY_VERSION_NONE);
+ RunThroughFilter("b.html", 310, 2, mod_spdy::spdy::SPDY_VERSION_NONE);
+ RunThroughFilter("c.html", 320, 2, mod_spdy::spdy::SPDY_VERSION_NONE);
+
+ ap_filter_t* const ap_filter(RunThroughFilter(
+ "index.html", 350, -1, mod_spdy::spdy::SPDY_VERSION_3_1));
+ EXPECT_EQ("\"b.html\":2,\"c.html\":5,", GetXACHeader(ap_filter));
+}
+
+} // namespace
Added: httpd/mod_spdy/branches/httpd-2.2.x/mod_spdy/apache/filters/server_push_filter.cc
URL: http://svn.apache.org/viewvc/httpd/mod_spdy/branches/httpd-2.2.x/mod_spdy/apache/filters/server_push_filter.cc?rev=1591620&view=auto
==============================================================================
--- httpd/mod_spdy/branches/httpd-2.2.x/mod_spdy/apache/filters/server_push_filter.cc (added)
+++ httpd/mod_spdy/branches/httpd-2.2.x/mod_spdy/apache/filters/server_push_filter.cc Thu May 1 11:39:27 2014
@@ -0,0 +1,263 @@
+// Copyright 2012 Google Inc.
+//
+// Licensed 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 "mod_spdy/apache/filters/server_push_filter.h"
+
+#include <string>
+
+#include "base/logging.h"
+#include "base/strings/string_number_conversions.h" // for StringToUint
+#include "base/strings/string_piece.h"
+#include "mod_spdy/common/protocol_util.h"
+#include "mod_spdy/common/spdy_server_config.h"
+#include "mod_spdy/common/spdy_stream.h"
+
+namespace mod_spdy {
+
+namespace {
+
+// Utility function passed to apr_table_do:
+int AddOneHeader(void* headers, const char* key, const char* value) {
+ mod_spdy::MergeInHeader(
+ key, value, static_cast<net::SpdyHeaderBlock*>(headers));
+ return 1; // return zero to stop, or non-zero to continue iterating
+}
+
+// Modify *source to remove whitespace characters from the front.
+void AbsorbWhiteSpace(base::StringPiece* source) {
+ *source = source->substr(source->find_first_not_of(" \n\r\t"));
+}
+
+// If the first thing in *source is '"foobar"', set out to 'foobar', modify
+// *source to skip past both quotes and any whitespace thereafter, and return
+// true. Otherwise return false.
+bool ParseQuotedString(base::StringPiece* source, std::string* out) {
+ if (source->empty() || (*source)[0] != '"') {
+ return false; // failure: no open quote
+ }
+ const size_t close = source->find('"', 1);
+ if (close == base::StringPiece::npos) {
+ return false; // failure: no close quote
+ }
+ source->substr(1, close - 1).CopyToString(out);
+ *source = source->substr(close + 1);
+ AbsorbWhiteSpace(source);
+ return true;
+}
+
+// If the next character in *source is c, modify *source to skip past the
+// character and any whitespace thereafter, and return true. Otherwise return
+// false.
+bool ParseSeparator(char c, base::StringPiece* source) {
+ if (source->empty() || (*source)[0] != c) {
+ return false;
+ }
+ *source = source->substr(1);
+ AbsorbWhiteSpace(source);
+ return true;
+}
+
+// If the next part of *source looks like ':2' (for some value of 2), parse the
+// number, store it in *out, and modify *source to skip past it. Otherwise,
+// just leave *source unchanged. See ParseAssociatedContent for the full
+// expected format of *source.
+net::SpdyPriority ParseOptionalPriority(SpdyStream* spdy_stream,
+ base::StringPiece* source) {
+ const net::SpdyPriority lowest_priority =
+ LowestSpdyPriorityForVersion(spdy_stream->spdy_version());
+ if (!ParseSeparator(':', source)) {
+ // It's okay for the ":priority" to not be there. In that case, we default
+ // to minimal priority.
+ return lowest_priority;
+ }
+ const size_t end = source->find_first_not_of("0123456789");
+ const base::StringPiece number = source->substr(0, end);
+ unsigned priority;
+ if (!StringToUint(number, &priority)) {
+ LOG(INFO) << "Invalid priority value in X-Associated-Content: '"
+ << number << "'";
+ return lowest_priority;
+ }
+ *source = source->substr(end);
+ AbsorbWhiteSpace(source);
+ // Clamp the priority to a legal value (larger numbers represent lower
+ // priorities, so we must not return a number greater than lowest_priority).
+ return (priority > lowest_priority ? lowest_priority : priority);
+}
+
+} // namespace
+
+ServerPushFilter::ServerPushFilter(SpdyStream* stream, request_rec* request,
+ const SpdyServerConfig* server_cfg)
+ : stream_(stream), request_(request), server_cfg_(server_cfg) {
+ DCHECK(stream_);
+ DCHECK(request_);
+}
+
+ServerPushFilter::~ServerPushFilter() {}
+
+apr_status_t ServerPushFilter::Write(ap_filter_t* filter,
+ apr_bucket_brigade* input_brigade) {
+ DCHECK_EQ(request_, filter->r);
+ // We only do server pushes for SPDY v3 and later. Also, to avoid infinite
+ // push loops, we don't allow push streams to invoke further push streams
+ // beyond a specified depth.
+ if (stream_->spdy_version() >= spdy::SPDY_VERSION_3 &&
+ stream_->server_push_depth() < server_cfg_->max_server_push_depth()) {
+ // Parse and start pushes for each X-Associated-Content header, if any.
+ // (Note that APR tables allow multiple entries with the same key, just
+ // like HTTP headers.)
+ apr_table_do(
+ OnXAssociatedContent, // function to call on each key/value pair
+ this, // void* to be passed as first arg to function
+ request_->headers_out, // the apr_table_t to iterate over
+ // Varargs: zero or more char* keys to iterate over, followed by NULL
+ http::kXAssociatedContent, NULL);
+ // We need to give the same treatment to err_headers_out as we just gave to
+ // headers_out. Depending on how the X-Associated-Content header was set,
+ // it might end up in either one. For example, using a mod_headers Header
+ // directive will put the header in headers_out, but using a PHP header()
+ // function call will put the header in err_headers_out.
+ apr_table_do(OnXAssociatedContent, this, request_->err_headers_out,
+ http::kXAssociatedContent, NULL);
+ }
+ // Even in cases where we forbid pushes from this stream, we still purge the
+ // X-Associated-Content header (from both headers_out and err_headers_out).
+ apr_table_unset(request_->headers_out, http::kXAssociatedContent);
+ apr_table_unset(request_->err_headers_out, http::kXAssociatedContent);
+
+ // Remove ourselves from the filter chain.
+ ap_remove_output_filter(filter);
+ // Pass the data through unchanged.
+ return ap_pass_brigade(filter->next, input_brigade);
+}
+
+void ServerPushFilter::ParseXAssociatedContentHeader(base::StringPiece value) {
+ AbsorbWhiteSpace(&value);
+ bool first_url = true;
+
+ while (!value.empty()) {
+ // The URLs should be separated by commas, so a comma should proceed each
+ // URL except the first.
+ if (first_url) {
+ first_url = false;
+ } else if (!ParseSeparator(',', &value)) {
+ LOG(INFO) << "Parse error in X-Associated-Content: missing comma";
+ return;
+ }
+
+ // Get a quoted URL string.
+ std::string url;
+ if (!ParseQuotedString(&value, &url)) {
+ LOG(INFO) << "Parse error in X-Associated-Content: expected quoted URL";
+ return;
+ }
+
+ // The URL may optionally be followed by a priority. If the priority is
+ // not there, use the lowest-importance priority by default.
+ net::SpdyPriority priority = ParseOptionalPriority(stream_, &value);
+
+ // Try to parse the URL string. If it does not form a valid URL, log an
+ // error and skip past this entry.
+ apr_uri_t parsed_url;
+ {
+ const apr_status_t status =
+ apr_uri_parse(request_->pool, url.c_str(), &parsed_url);
+ if (status != APR_SUCCESS) {
+ LOG(ERROR) << "Invalid URL in X-Associated-Content: '" << url << "'";
+ continue;
+ }
+ }
+
+ // Populate the fake request headers for the pushed stream.
+ net::SpdyHeaderBlock request_headers;
+ // Start off by pulling in certain headers from the associated stream's
+ // request headers.
+ apr_table_do(
+ AddOneHeader, // function to call on each key/value pair
+ &request_headers, // void* to be passed as first arg to function
+ request_->headers_in, // the apr_table_t to iterate over
+ // Varargs: zero or more char* keys to iterate over, followed by NULL
+ "accept", "accept-charset", "accept-datetime",
+ mod_spdy::http::kAcceptEncoding, "accept-language", "authorization",
+ "user-agent", NULL);
+ // Next, we populate special SPDY headers, using a combination of pushed
+ // URL and details from the associated request.
+ if (parsed_url.hostinfo != NULL) {
+ request_headers[spdy::kSpdy3Host] = parsed_url.hostinfo;
+ } else {
+ const char* host_header =
+ apr_table_get(request_->headers_in, http::kHost);
+ request_headers[spdy::kSpdy3Host] =
+ (host_header != NULL ? host_header :
+ request_->hostname != NULL ? request_->hostname : "");
+ }
+ request_headers[spdy::kSpdy3Method] = "GET";
+ request_headers[spdy::kSpdy3Scheme] =
+ (parsed_url.scheme != NULL ? parsed_url.scheme : "https");
+ request_headers[spdy::kSpdy3Version] = request_->protocol;
+ // Construct the path that we are pushing from the parsed URL.
+ // TODO(mdsteele): It'd be nice to support relative URLs.
+ {
+ std::string* path = &request_headers[spdy::kSpdy3Path];
+ path->assign(parsed_url.path == NULL ? "/" : parsed_url.path);
+ if (parsed_url.query != NULL) {
+ path->push_back('?');
+ path->append(parsed_url.query);
+ }
+ if (parsed_url.fragment != NULL) {
+ // It's a little weird to try to push a URL with a fragment in it, but
+ // if someone does so anyway, we may as well honor it.
+ path->push_back('#');
+ path->append(parsed_url.fragment);
+ }
+ }
+ // Finally, we set the HTTP referrer to be the associated stream's URL.
+ request_headers[http::kReferer] = request_->unparsed_uri;
+
+ // Try to perform the push. If it succeeds, we'll continue with parsing.
+ const SpdyServerPushInterface::PushStatus status =
+ stream_->StartServerPush(priority, request_headers);
+ switch (status) {
+ case SpdyServerPushInterface::PUSH_STARTED:
+ break; // success
+ case SpdyServerPushInterface::INVALID_REQUEST_HEADERS:
+ // This shouldn't happen unless there's a bug in the above code.
+ LOG(DFATAL) << "ParseAssociatedContent: invalid request headers";
+ return;
+ case SpdyServerPushInterface::ASSOCIATED_STREAM_INACTIVE:
+ case SpdyServerPushInterface::CANNOT_PUSH_EVER_AGAIN:
+ case SpdyServerPushInterface::TOO_MANY_CONCURRENT_PUSHES:
+ case SpdyServerPushInterface::PUSH_INTERNAL_ERROR:
+ // In any of these cases, any remaining pushes specified by the header
+ // are unlikely to succeed, so just stop parsing and quit.
+ LOG(INFO) << "Push failed while processing X-Associated-Content "
+ << "header (status=" << status << "). Skipping remainder.";
+ return;
+ default:
+ LOG(DFATAL) << "Invalid push status value: " << status;
+ return;
+ }
+ }
+}
+
+// static
+int ServerPushFilter::OnXAssociatedContent(
+ void* server_push_filter, const char* key, const char* value) {
+ static_cast<ServerPushFilter*>(server_push_filter)->
+ ParseXAssociatedContentHeader(value);
+ return 1; // return zero to stop, or non-zero to continue iterating
+}
+
+} // namespace mod_spdy
Added: httpd/mod_spdy/branches/httpd-2.2.x/mod_spdy/apache/filters/server_push_filter.h
URL: http://svn.apache.org/viewvc/httpd/mod_spdy/branches/httpd-2.2.x/mod_spdy/apache/filters/server_push_filter.h?rev=1591620&view=auto
==============================================================================
--- httpd/mod_spdy/branches/httpd-2.2.x/mod_spdy/apache/filters/server_push_filter.h (added)
+++ httpd/mod_spdy/branches/httpd-2.2.x/mod_spdy/apache/filters/server_push_filter.h Thu May 1 11:39:27 2014
@@ -0,0 +1,73 @@
+// Copyright 2012 Google Inc.
+//
+// Licensed 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.
+
+#ifndef MOD_SPDY_APACHE_FILTERS_SERVER_PUSH_FILTER_H_
+#define MOD_SPDY_APACHE_FILTERS_SERVER_PUSH_FILTER_H_
+
+#include <string>
+
+#include "apr_buckets.h"
+#include "util_filter.h"
+
+#include "base/basictypes.h"
+#include "base/strings/string_piece.h"
+#include "mod_spdy/common/spdy_server_config.h"
+
+
+namespace mod_spdy {
+
+class SpdyStream;
+
+// An Apache filter for initiating SPDY server pushes based on the
+// X-Associated-Content response header, which may be set by e.g. mod_headers
+// or a CGI script.
+class ServerPushFilter {
+ public:
+ // Does not take ownership of either argument.
+ ServerPushFilter(SpdyStream* stream, request_rec* request,
+ const SpdyServerConfig* server_cfg);
+ ~ServerPushFilter();
+
+ // Read data from the given brigade and write the result through the given
+ // filter. This filter doesn't touch the data; it only looks at the request
+ // headers.
+ apr_status_t Write(ap_filter_t* filter, apr_bucket_brigade* input_brigade);
+
+ private:
+ // Parse the value of an X-Associated-Content header, and initiate any
+ // specified server pushes. The expected format of the header value is a
+ // comma-separated list of quoted URLs, each of which may optionally be
+ // followed by colon and a SPDY priority value. The URLs may be fully
+ // qualified URLs, or simply absolute paths (for the same scheme/host as the
+ // original request). If the optional priority is omitted for a URL, then it
+ // uses the lowest possible priority. Whitespace is permitted between
+ // tokens. For example:
+ //
+ // X-Associated-Content: "https://www.example.com/foo.css",
+ // "/bar/baz.js?x=y" : 1, "https://www.example.com/quux.css":3
+ void ParseXAssociatedContentHeader(base::StringPiece value);
+ // Utility function passed to apr_table_do. The first argument must point to
+ // a ServerPushFilter; this will call ParseXAssociatedContentHeader on it.
+ static int OnXAssociatedContent(void*, const char*, const char*);
+
+ SpdyStream* const stream_;
+ request_rec* const request_;
+ const SpdyServerConfig* server_cfg_;
+
+ DISALLOW_COPY_AND_ASSIGN(ServerPushFilter);
+};
+
+} // namespace mod_spdy
+
+#endif // MOD_SPDY_APACHE_FILTERS_SERVER_PUSH_FILTER_H_
Propchange: httpd/mod_spdy/branches/httpd-2.2.x/mod_spdy/apache/filters/server_push_filter.h
------------------------------------------------------------------------------
svn:eol-style = native
Added: httpd/mod_spdy/branches/httpd-2.2.x/mod_spdy/apache/filters/server_push_filter_test.cc
URL: http://svn.apache.org/viewvc/httpd/mod_spdy/branches/httpd-2.2.x/mod_spdy/apache/filters/server_push_filter_test.cc?rev=1591620&view=auto
==============================================================================
--- httpd/mod_spdy/branches/httpd-2.2.x/mod_spdy/apache/filters/server_push_filter_test.cc (added)
+++ httpd/mod_spdy/branches/httpd-2.2.x/mod_spdy/apache/filters/server_push_filter_test.cc Thu May 1 11:39:27 2014
@@ -0,0 +1,489 @@
+// Copyright 2012 Google Inc.
+//
+// Licensed 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 "mod_spdy/apache/filters/server_push_filter.h"
+
+#include <string>
+
+#include "httpd.h"
+#include "apr_buckets.h"
+#include "apr_tables.h"
+#include "util_filter.h"
+
+#include "base/strings/string_piece.h"
+#include "mod_spdy/apache/pool_util.h"
+#include "mod_spdy/common/protocol_util.h"
+#include "mod_spdy/common/shared_flow_control_window.h"
+#include "mod_spdy/common/spdy_frame_priority_queue.h"
+#include "mod_spdy/common/spdy_server_config.h"
+#include "mod_spdy/common/spdy_stream.h"
+#include "net/spdy/spdy_framer.h"
+#include "net/spdy/spdy_protocol.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using testing::_;
+using testing::Contains;
+using testing::Eq;
+using testing::Pair;
+using testing::Return;
+
+namespace {
+
+const char* const kRefererUrl = "https://www.example.com/index.html";
+
+class MockSpdyServerPushInterface : public mod_spdy::SpdyServerPushInterface {
+ public:
+ MOCK_METHOD4(StartServerPush,
+ mod_spdy::SpdyServerPushInterface::PushStatus(
+ net::SpdyStreamId associated_stream_id,
+ int32 server_push_depth,
+ net::SpdyPriority priority,
+ const net::SpdyHeaderBlock& request_headers));
+};
+
+class ServerPushFilterTest :
+ public testing::TestWithParam<mod_spdy::spdy::SpdyVersion> {
+ public:
+ ServerPushFilterTest()
+ : spdy_version_(GetParam()),
+ shared_window_(net::kSpdyStreamInitialWindowSize,
+ net::kSpdyStreamInitialWindowSize),
+ connection_(static_cast<conn_rec*>(
+ apr_pcalloc(local_.pool(), sizeof(conn_rec)))),
+ request_(static_cast<request_rec*>(
+ apr_pcalloc(local_.pool(), sizeof(request_rec)))),
+ ap_filter_(static_cast<ap_filter_t*>(
+ apr_pcalloc(local_.pool(), sizeof(ap_filter_t)))),
+ bucket_alloc_(apr_bucket_alloc_create(local_.pool())),
+ brigade_(apr_brigade_create(local_.pool(), bucket_alloc_)) {
+ // Set up our Apache data structures. To keep things simple, we set only
+ // the bare minimum of necessary fields, and rely on apr_pcalloc to zero
+ // all others.
+ connection_->pool = local_.pool();
+ request_->pool = local_.pool();
+ request_->connection = connection_;
+ request_->headers_in = apr_table_make(local_.pool(), 5);
+ request_->headers_out = apr_table_make(local_.pool(), 5);
+ request_->err_headers_out = apr_table_make(local_.pool(), 5);
+ request_->protocol = const_cast<char*>("HTTP/1.1");
+ request_->unparsed_uri = const_cast<char*>(kRefererUrl);
+ ap_filter_->c = connection_;
+ ap_filter_->r = request_;
+ }
+
+ virtual void SetUp() {
+ ON_CALL(pusher_, StartServerPush(_, _, _, _)).WillByDefault(
+ Return(mod_spdy::SpdyServerPushInterface::PUSH_STARTED));
+ apr_table_setn(request_->headers_in, mod_spdy::http::kHost,
+ "www.example.com");
+ }
+
+ protected:
+ void WriteBrigade(mod_spdy::ServerPushFilter* filter) {
+ EXPECT_EQ(APR_SUCCESS, filter->Write(ap_filter_, brigade_));
+ }
+
+ const char* status_header_name() const {
+ return (spdy_version_ < mod_spdy::spdy::SPDY_VERSION_3 ?
+ mod_spdy::spdy::kSpdy2Status : mod_spdy::spdy::kSpdy3Status);
+ }
+
+ const char* version_header_name() const {
+ return (spdy_version_ < mod_spdy::spdy::SPDY_VERSION_3 ?
+ mod_spdy::spdy::kSpdy2Version : mod_spdy::spdy::kSpdy3Version);
+ }
+
+ const mod_spdy::spdy::SpdyVersion spdy_version_;
+ mod_spdy::SpdyFramePriorityQueue output_queue_;
+ mod_spdy::SharedFlowControlWindow shared_window_;
+ MockSpdyServerPushInterface pusher_;
+ mod_spdy::LocalPool local_;
+ conn_rec* const connection_;
+ request_rec* const request_;
+ ap_filter_t* const ap_filter_;
+ apr_bucket_alloc_t* const bucket_alloc_;
+ apr_bucket_brigade* const brigade_;
+};
+
+TEST_P(ServerPushFilterTest, SimpleXAssociatedContent) {
+ const net::SpdyStreamId stream_id = 3;
+ const net::SpdyStreamId associated_stream_id = 0;
+ const int32 initial_server_push_depth = 0;
+ const net::SpdyPriority priority = 1;
+ const mod_spdy::SpdyServerConfig server_cfg;
+ mod_spdy::SpdyStream stream(
+ spdy_version_, stream_id, associated_stream_id,
+ initial_server_push_depth, priority, net::kSpdyStreamInitialWindowSize,
+ &output_queue_, &shared_window_, &pusher_);
+ mod_spdy::ServerPushFilter server_push_filter(&stream, request_, &server_cfg);
+
+ net::SpdyHeaderBlock headers1;
+ headers1[mod_spdy::spdy::kSpdy3Host] = "www.example.com";
+ headers1[mod_spdy::spdy::kSpdy3Method] = "GET";
+ headers1[mod_spdy::spdy::kSpdy3Path] = "/foo/bar.css?q=12";
+ headers1[mod_spdy::spdy::kSpdy3Scheme] = "https";
+ headers1[mod_spdy::spdy::kSpdy3Version] = "HTTP/1.1";
+ headers1[mod_spdy::http::kReferer] = kRefererUrl;
+ EXPECT_CALL(pusher_, StartServerPush(
+ Eq(stream_id), Eq(initial_server_push_depth + 1), Eq(2u), Eq(headers1)));
+
+ net::SpdyHeaderBlock headers2;
+ headers2[mod_spdy::spdy::kSpdy3Host] = "cdn.example.com:8080";
+ headers2[mod_spdy::spdy::kSpdy3Method] = "GET";
+ headers2[mod_spdy::spdy::kSpdy3Path] = "/images/foo.png";
+ headers2[mod_spdy::spdy::kSpdy3Scheme] = "https";
+ headers2[mod_spdy::spdy::kSpdy3Version] = "HTTP/1.1";
+ headers2[mod_spdy::http::kReferer] = kRefererUrl;
+ EXPECT_CALL(pusher_, StartServerPush(
+ Eq(stream_id), Eq(initial_server_push_depth + 1), Eq(7u), Eq(headers2)));
+
+ net::SpdyHeaderBlock headers3;
+ headers3[mod_spdy::spdy::kSpdy3Host] = "www.example.com";
+ headers3[mod_spdy::spdy::kSpdy3Method] = "GET";
+ headers3[mod_spdy::spdy::kSpdy3Path] = "/scripts/awesome.js";
+ headers3[mod_spdy::spdy::kSpdy3Scheme] = "https";
+ headers3[mod_spdy::spdy::kSpdy3Version] = "HTTP/1.1";
+ headers3[mod_spdy::http::kReferer] = kRefererUrl;
+ EXPECT_CALL(pusher_, StartServerPush(
+ Eq(stream_id), Eq(initial_server_push_depth + 1), Eq(0u), Eq(headers3)));
+
+ apr_table_setn(request_->headers_out, mod_spdy::http::kXAssociatedContent,
+ "\"https://www.example.com/foo/bar.css?q=12\":2,"
+ "\"https://cdn.example.com:8080/images/foo.png\","
+ "\"/scripts/awesome.js\":0");
+ WriteBrigade(&server_push_filter);
+ // The X-Associated-Content header should get removed.
+ EXPECT_TRUE(apr_table_get(request_->headers_out,
+ mod_spdy::http::kXAssociatedContent) == NULL);
+}
+
+// Test that if there are multiple X-Associated-Content headers in the APR
+// table, we heed (and then remove) all of them.
+TEST_P(ServerPushFilterTest, MultipleXAssociatedContentHeaders) {
+ const net::SpdyStreamId stream_id = 13;
+ const net::SpdyStreamId associated_stream_id = 0;
+ const int32 initial_server_push_depth = 0;
+ const net::SpdyPriority priority = 1;
+ const mod_spdy::SpdyServerConfig server_cfg;
+ mod_spdy::SpdyStream stream(
+ spdy_version_, stream_id, associated_stream_id,
+ initial_server_push_depth, priority, net::kSpdyStreamInitialWindowSize,
+ &output_queue_, &shared_window_, &pusher_);
+ mod_spdy::ServerPushFilter server_push_filter(&stream, request_, &server_cfg);
+ const net::SpdyPriority lowest =
+ mod_spdy::LowestSpdyPriorityForVersion(stream.spdy_version());
+
+ testing::Sequence s1, s2, s3, s4, s5;
+
+ EXPECT_CALL(pusher_, StartServerPush(
+ Eq(stream_id), Eq(initial_server_push_depth + 1), Eq(2u),
+ Contains(Pair(mod_spdy::spdy::kSpdy3Path, "/x1.png")))).InSequence(s1);
+ EXPECT_CALL(pusher_, StartServerPush(
+ Eq(stream_id), Eq(initial_server_push_depth + 1), Eq(0u),
+ Contains(Pair(mod_spdy::spdy::kSpdy3Path, "/x2.png")))).InSequence(s1);
+
+ EXPECT_CALL(pusher_, StartServerPush(
+ Eq(stream_id), Eq(initial_server_push_depth + 1), Eq(lowest),
+ Contains(Pair(mod_spdy::spdy::kSpdy3Path, "/x3.png")))).InSequence(s2);
+
+ EXPECT_CALL(pusher_, StartServerPush(
+ Eq(stream_id), Eq(initial_server_push_depth + 1), Eq(3u),
+ Contains(Pair(mod_spdy::spdy::kSpdy3Path, "/x4.png")))).InSequence(s3);
+ EXPECT_CALL(pusher_, StartServerPush(
+ Eq(stream_id), Eq(initial_server_push_depth + 1), Eq(lowest),
+ Contains(Pair(mod_spdy::spdy::kSpdy3Path, "/x5.png")))).InSequence(s3);
+ EXPECT_CALL(pusher_, StartServerPush(
+ Eq(stream_id), Eq(initial_server_push_depth + 1), Eq(1u),
+ Contains(Pair(mod_spdy::spdy::kSpdy3Path, "/x6.png")))).InSequence(s3);
+
+ EXPECT_CALL(pusher_, StartServerPush(
+ Eq(stream_id), Eq(initial_server_push_depth + 1), Eq(2u),
+ Contains(Pair(mod_spdy::spdy::kSpdy3Path, "/x7.png")))).InSequence(s4);
+
+ EXPECT_CALL(pusher_, StartServerPush(
+ Eq(stream_id), Eq(initial_server_push_depth + 1), Eq(lowest),
+ Contains(Pair(mod_spdy::spdy::kSpdy3Path, "/x8.png")))).InSequence(s5);
+
+ // Add multiple X-Associated-Content headers to both headers_out and
+ // err_headers_out.
+ apr_table_addn(request_->headers_out, mod_spdy::http::kXAssociatedContent,
+ "\"/x1.png\":2, \"/x2.png\":0");
+ apr_table_addn(request_->headers_out, mod_spdy::http::kXAssociatedContent,
+ "\"/x3.png\"");
+ apr_table_addn(request_->headers_out, mod_spdy::http::kXAssociatedContent,
+ "\"/x4.png\":3, \"/x5.png\", \"/x6.png\":1");
+ apr_table_addn(request_->err_headers_out, mod_spdy::http::kXAssociatedContent,
+ "\"/x7.png\" : 2");
+ apr_table_addn(request_->err_headers_out, mod_spdy::http::kXAssociatedContent,
+ "\"/x8.png\"");
+
+ WriteBrigade(&server_push_filter);
+ // All the X-Associated-Content headers should get removed.
+ EXPECT_TRUE(apr_table_get(request_->headers_out,
+ mod_spdy::http::kXAssociatedContent) == NULL);
+ EXPECT_TRUE(apr_table_get(request_->err_headers_out,
+ mod_spdy::http::kXAssociatedContent) == NULL);
+}
+
+// Test that header key matching is case-insensitive.
+TEST_P(ServerPushFilterTest, CaseInsensitive) {
+ const net::SpdyStreamId stream_id = 13;
+ const net::SpdyStreamId associated_stream_id = 0;
+ const int32 initial_server_push_depth = 0;
+ const net::SpdyPriority priority = 1;
+ const mod_spdy::SpdyServerConfig server_cfg;
+ mod_spdy::SpdyStream stream(
+ spdy_version_, stream_id, associated_stream_id,
+ initial_server_push_depth, priority, net::kSpdyStreamInitialWindowSize,
+ &output_queue_, &shared_window_, &pusher_);
+ mod_spdy::ServerPushFilter server_push_filter(&stream, request_, &server_cfg);
+
+ EXPECT_CALL(pusher_, StartServerPush(Eq(stream_id), _, _, Contains(
+ Pair(mod_spdy::spdy::kSpdy3Path, "/x1.png"))));
+ EXPECT_CALL(pusher_, StartServerPush(Eq(stream_id), _, _, Contains(
+ Pair(mod_spdy::spdy::kSpdy3Path, "/x2.png"))));
+ EXPECT_CALL(pusher_, StartServerPush(Eq(stream_id), _, _, Contains(
+ Pair(mod_spdy::spdy::kSpdy3Path, "/x3.png"))));
+
+ apr_table_addn(request_->headers_out, "X-Associated-Content", "\"/x1.png\"");
+ apr_table_addn(request_->headers_out, "X-ASSOCIATED-CONTENT", "\"/x2.png\"");
+ apr_table_addn(request_->headers_out, "x-AsSoCiAtEd-cOnTeNt", "\"/x3.png\"");
+
+ WriteBrigade(&server_push_filter);
+ // All three X-Associated-Content headers should get removed, despite their
+ // weird capitalization. (Note that apr_table_get is itself
+ // case-insensitive, but we explicitly check all the variations here anyway,
+ // just to be sure.)
+ EXPECT_TRUE(apr_table_get(request_->headers_out,
+ mod_spdy::http::kXAssociatedContent) == NULL);
+ EXPECT_TRUE(apr_table_get(request_->headers_out,
+ "X-Associated-Content") == NULL);
+ EXPECT_TRUE(apr_table_get(request_->headers_out,
+ "X-ASSOCIATED-CONTENT") == NULL);
+ EXPECT_TRUE(apr_table_get(request_->headers_out,
+ "x-AsSoCiAtEd-cOnTeNt") == NULL);
+}
+
+TEST_P(ServerPushFilterTest, CopyApplicableHeaders) {
+ const net::SpdyStreamId stream_id = 7;
+ const net::SpdyStreamId associated_stream_id = 0;
+ const int32 initial_server_push_depth = 0;
+ const net::SpdyPriority priority = 0;
+ const mod_spdy::SpdyServerConfig server_cfg;
+ mod_spdy::SpdyStream stream(
+ spdy_version_, stream_id, associated_stream_id,
+ initial_server_push_depth, priority, net::kSpdyStreamInitialWindowSize,
+ &output_queue_, &shared_window_, &pusher_);
+ mod_spdy::ServerPushFilter server_push_filter(&stream, request_, &server_cfg);
+
+ // Set some extra headers on the original request (which was evidentally a
+ // POST). The Accept-Language header should get copied over for the push,
+ // but the Content-Length header obviously should not.
+ apr_table_setn(request_->headers_in, "accept-language", "en-US");
+ apr_table_setn(request_->headers_in, "content-length", "200");
+
+ net::SpdyHeaderBlock headers1;
+ headers1[mod_spdy::spdy::kSpdy3Host] = "www.example.com";
+ headers1[mod_spdy::spdy::kSpdy3Method] = "GET";
+ headers1[mod_spdy::spdy::kSpdy3Path] = "/foo/bar.css";
+ headers1[mod_spdy::spdy::kSpdy3Scheme] = "https";
+ headers1[mod_spdy::spdy::kSpdy3Version] = "HTTP/1.1";
+ headers1[mod_spdy::http::kReferer] = kRefererUrl;
+ headers1["accept-language"] = "en-US";
+ EXPECT_CALL(pusher_, StartServerPush(
+ Eq(stream_id), Eq(initial_server_push_depth + 1), Eq(2u), Eq(headers1)));
+
+ apr_table_setn(request_->headers_out, mod_spdy::http::kXAssociatedContent,
+ " \"https://www.example.com/foo/bar.css\" : 2 ");
+ WriteBrigade(&server_push_filter);
+}
+
+TEST_P(ServerPushFilterTest, StopPushingAfterPushError) {
+ const net::SpdyStreamId stream_id = 3;
+ const net::SpdyStreamId associated_stream_id = 0;
+ const int32 initial_server_push_depth = 0;
+ const net::SpdyPriority priority = 1;
+ const mod_spdy::SpdyServerConfig server_cfg;
+ mod_spdy::SpdyStream stream(
+ spdy_version_, stream_id, associated_stream_id,
+ initial_server_push_depth, priority, net::kSpdyStreamInitialWindowSize,
+ &output_queue_, &shared_window_, &pusher_);
+ mod_spdy::ServerPushFilter server_push_filter(&stream, request_, &server_cfg);
+
+ // When the filter tries to push the first resource, we reply that pushes are
+ // no longer possible on this connection. The filter should not attempt any
+ // more pushes, even though more were specified.
+ EXPECT_CALL(pusher_, StartServerPush(
+ Eq(stream_id),
+ Eq(initial_server_push_depth + 1),
+ Eq(2u), _)).WillOnce(
+ Return(mod_spdy::SpdyServerPushInterface::CANNOT_PUSH_EVER_AGAIN));
+
+ apr_table_setn(request_->headers_out, mod_spdy::http::kXAssociatedContent,
+ "\"https://www.example.com/foo/bar.css?q=12\":2,"
+ "\"cdn.example.com:8080/images/foo.png\","
+ "\"/scripts/awesome.js\":0");
+ WriteBrigade(&server_push_filter);
+ // The X-Associated-Content header should still get removed, though.
+ EXPECT_TRUE(apr_table_get(request_->headers_out,
+ mod_spdy::http::kXAssociatedContent) == NULL);
+}
+
+TEST_P(ServerPushFilterTest, StopPushingAfterParseError) {
+ const net::SpdyStreamId stream_id = 3;
+ const net::SpdyStreamId associated_stream_id = 0;
+ const int32 initial_server_push_depth = 0;
+ const net::SpdyPriority priority = 1;
+ const mod_spdy::SpdyServerConfig server_cfg;
+ mod_spdy::SpdyStream stream(
+ spdy_version_, stream_id, associated_stream_id,
+ initial_server_push_depth, priority, net::kSpdyStreamInitialWindowSize,
+ &output_queue_, &shared_window_, &pusher_);
+ mod_spdy::ServerPushFilter server_push_filter(&stream, request_, &server_cfg);
+
+ // The filter should push the first resource, but then stop when it gets to
+ // the parse error.
+ EXPECT_CALL(pusher_, StartServerPush(
+ Eq(stream_id), Eq(initial_server_push_depth + 1), Eq(2u), _));
+
+ apr_table_setn(request_->headers_out, mod_spdy::http::kXAssociatedContent,
+ "\"https://www.example.com/foo/bar.css?q=12\":2,"
+ "oops.iforgot.to/quote/this/url.js,"
+ "\"/scripts/awesome.js\":0");
+ WriteBrigade(&server_push_filter);
+ // The X-Associated-Content header should still get removed, though.
+ EXPECT_TRUE(apr_table_get(request_->headers_out,
+ mod_spdy::http::kXAssociatedContent) == NULL);
+}
+
+TEST_P(ServerPushFilterTest, SkipInvalidQuotedUrl) {
+ const net::SpdyStreamId stream_id = 3;
+ const net::SpdyStreamId associated_stream_id = 0;
+ const int32 initial_server_push_depth = 0;
+ const net::SpdyPriority priority = 1;
+ const mod_spdy::SpdyServerConfig server_cfg;
+ mod_spdy::SpdyStream stream(
+ spdy_version_, stream_id, associated_stream_id,
+ initial_server_push_depth, priority, net::kSpdyStreamInitialWindowSize,
+ &output_queue_, &shared_window_, &pusher_);
+ mod_spdy::ServerPushFilter server_push_filter(&stream, request_, &server_cfg);
+
+ // The filter should push the first and third resources, but skip the second
+ // one because its quoted URL is invalid.
+ EXPECT_CALL(pusher_, StartServerPush(
+ Eq(stream_id), Eq(initial_server_push_depth + 1), Eq(2u), _));
+ EXPECT_CALL(pusher_, StartServerPush(
+ Eq(stream_id), Eq(initial_server_push_depth + 1), Eq(0u), _));
+
+ apr_table_setn(request_->headers_out, mod_spdy::http::kXAssociatedContent,
+ " \"https://www.example.com/foo/bar.css?q=12\" : 2, "
+ "\"https://this.is:not/a valid URL!\":1, "
+ "\"/scripts/awesome.js\":0 ");
+ WriteBrigade(&server_push_filter);
+ // The X-Associated-Content header should still get removed, though.
+ EXPECT_TRUE(apr_table_get(request_->headers_out,
+ mod_spdy::http::kXAssociatedContent) == NULL);
+}
+
+TEST_P(ServerPushFilterTest, MaxServerPushDepthLimit) {
+ const net::SpdyStreamId stream_id = 2;
+ const net::SpdyStreamId associated_stream_id = 5;
+ const int32 initial_server_push_depth = 0;
+ const net::SpdyPriority priority = 1;
+ mod_spdy::SpdyServerConfig server_cfg;
+
+ server_cfg.set_max_server_push_depth(0);
+ mod_spdy::SpdyStream stream(
+ spdy_version_, stream_id, associated_stream_id,
+ initial_server_push_depth, priority, net::kSpdyStreamInitialWindowSize,
+ &output_queue_, &shared_window_, &pusher_);
+ mod_spdy::ServerPushFilter server_push_filter(&stream, request_, &server_cfg);
+
+ // We should not get any calls to StartServerPush, because we do not allow
+ // server-pushed resources to push any more resources.
+ EXPECT_CALL(pusher_, StartServerPush(_,_,_,_)).Times(0);
+
+ apr_table_setn(request_->headers_out, mod_spdy::http::kXAssociatedContent,
+ "\"https://www.example.com/foo/bar.css?q=12\":2,"
+ "\"cdn.example.com:8080/images/foo.png\","
+ "\"/scripts/awesome.js\":0");
+ WriteBrigade(&server_push_filter);
+ // The X-Associated-Content header should still get removed, though.
+ EXPECT_TRUE(apr_table_get(request_->headers_out,
+ mod_spdy::http::kXAssociatedContent) == NULL);
+
+
+ // Now increase our max_server_push_depth, but also our
+ // initial_server_push_depth. We expect the same result.
+ const int32 initial_server_push_depth_2 = 5;
+ mod_spdy::SpdyServerConfig server_cfg_2;
+ server_cfg.set_max_server_push_depth(5);
+ mod_spdy::SpdyStream stream_2(
+ spdy_version_, stream_id, associated_stream_id,
+ initial_server_push_depth_2, priority, net::kSpdyStreamInitialWindowSize,
+ &output_queue_, &shared_window_, &pusher_);
+ mod_spdy::ServerPushFilter server_push_filter_2(
+ &stream_2, request_, &server_cfg_2);
+
+ // We should not get any calls to StartServerPush, because we do not allow
+ // server-pushed resources to push any more resources.
+ EXPECT_CALL(pusher_, StartServerPush(_,_,_,_)).Times(0);
+
+ apr_table_setn(request_->headers_out, mod_spdy::http::kXAssociatedContent,
+ "\"https://www.example.com/foo/bar.css?q=12\":2,"
+ "\"cdn.example.com:8080/images/foo.png\","
+ "\"/scripts/awesome.js\":0");
+ WriteBrigade(&server_push_filter_2);
+ // The X-Associated-mContent header should still get removed, though.
+ EXPECT_TRUE(apr_table_get(request_->headers_out,
+ mod_spdy::http::kXAssociatedContent) == NULL);
+}
+
+// Run server push tests only over SPDY v3.
+INSTANTIATE_TEST_CASE_P(Spdy3, ServerPushFilterTest, testing::Values(
+ mod_spdy::spdy::SPDY_VERSION_3, mod_spdy::spdy::SPDY_VERSION_3_1));
+
+// Create a type alias so that we can instantiate some of our
+// SpdySessionTest-based tests using a different set of parameters.
+typedef ServerPushFilterTest ServerPushFilterSpdy2Test;
+
+TEST_P(ServerPushFilterSpdy2Test, NoPushesForSpdy2) {
+ const net::SpdyStreamId stream_id = 3;
+ const net::SpdyStreamId associated_stream_id = 0;
+ const int32 initial_server_push_depth = 0;
+ const net::SpdyPriority priority = 1;
+ const mod_spdy::SpdyServerConfig server_cfg;
+ mod_spdy::SpdyStream stream(
+ spdy_version_, stream_id, associated_stream_id,
+ initial_server_push_depth, priority, net::kSpdyStreamInitialWindowSize,
+ &output_queue_, &shared_window_, &pusher_);
+ mod_spdy::ServerPushFilter server_push_filter(&stream, request_, &server_cfg);
+
+ // We should not get any calls to StartServerPush when we're on SPDY/2.
+
+ apr_table_setn(request_->headers_out, mod_spdy::http::kXAssociatedContent,
+ "\"https://www.example.com/foo/bar.css?q=12\":2,"
+ "\"cdn.example.com:8080/images/foo.png\","
+ "\"/scripts/awesome.js\":0");
+ WriteBrigade(&server_push_filter);
+ // The X-Associated-Content header should still get removed, though.
+ EXPECT_TRUE(apr_table_get(request_->headers_out,
+ mod_spdy::http::kXAssociatedContent) == NULL);
+}
+
+INSTANTIATE_TEST_CASE_P(Spdy2, ServerPushFilterSpdy2Test, testing::Values(
+ mod_spdy::spdy::SPDY_VERSION_2));
+
+} // namespace