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:43:45 UTC

svn commit: r1591622 [7/33] - in /httpd/mod_spdy/trunk: ./ base/ base/base.xcodeproj/ base/metrics/ build/ build/all.xcodeproj/ build/build_util.xcodeproj/ build/install.xcodeproj/ build/internal/ build/linux/ build/mac/ build/util/ build/win/ install/...

Added: httpd/mod_spdy/trunk/mod_spdy/apache/filters/http_to_spdy_filter.cc
URL: http://svn.apache.org/viewvc/httpd/mod_spdy/trunk/mod_spdy/apache/filters/http_to_spdy_filter.cc?rev=1591622&view=auto
==============================================================================
--- httpd/mod_spdy/trunk/mod_spdy/apache/filters/http_to_spdy_filter.cc (added)
+++ httpd/mod_spdy/trunk/mod_spdy/apache/filters/http_to_spdy_filter.cc Thu May  1 11:43:36 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/trunk/mod_spdy/apache/filters/http_to_spdy_filter.h
URL: http://svn.apache.org/viewvc/httpd/mod_spdy/trunk/mod_spdy/apache/filters/http_to_spdy_filter.h?rev=1591622&view=auto
==============================================================================
--- httpd/mod_spdy/trunk/mod_spdy/apache/filters/http_to_spdy_filter.h (added)
+++ httpd/mod_spdy/trunk/mod_spdy/apache/filters/http_to_spdy_filter.h Thu May  1 11:43:36 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/trunk/mod_spdy/apache/filters/http_to_spdy_filter.h
------------------------------------------------------------------------------
    svn:eol-style = native

Added: httpd/mod_spdy/trunk/mod_spdy/apache/filters/http_to_spdy_filter_test.cc
URL: http://svn.apache.org/viewvc/httpd/mod_spdy/trunk/mod_spdy/apache/filters/http_to_spdy_filter_test.cc?rev=1591622&view=auto
==============================================================================
--- httpd/mod_spdy/trunk/mod_spdy/apache/filters/http_to_spdy_filter_test.cc (added)
+++ httpd/mod_spdy/trunk/mod_spdy/apache/filters/http_to_spdy_filter_test.cc Thu May  1 11:43:36 2014
@@ -0,0 +1,538 @@
+// 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/string_piece.h"
+#include "mod_spdy/apache/pool_util.h"
+#include "mod_spdy/common/protocol_util.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/buffered_spdy_framer.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 mod_spdy::testing::FlagFinIs;
+using mod_spdy::testing::IsControlFrameOfType;
+using mod_spdy::testing::IsDataFrame;
+using mod_spdy::testing::IsDataFrameWith;
+using mod_spdy::testing::StreamIdIs;
+using mod_spdy::testing::UncompressedHeadersAre;
+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()),
+        framer_(mod_spdy::SpdyVersionToFramerVersion(spdy_version_)),
+        buffered_framer_(mod_spdy::SpdyVersionToFramerVersion(spdy_version_)),
+        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::SpdyFrame* raw_frame = NULL;
+    ASSERT_TRUE(output_queue_.Pop(&raw_frame));
+    ASSERT_TRUE(raw_frame != NULL);
+    scoped_ptr<net::SpdyFrame> frame(raw_frame);
+    EXPECT_THAT(*frame, AllOf(IsControlFrameOfType(net::SYN_REPLY),
+                              StreamIdIs(stream_id),
+                              UncompressedHeadersAre(headers),
+                              FlagFinIs(flag_fin)));
+  }
+
+  void ExpectHeaders(net::SpdyStreamId stream_id,
+                     const net::SpdyHeaderBlock& headers,
+                     bool flag_fin) {
+    net::SpdyFrame* raw_frame = NULL;
+    ASSERT_TRUE(output_queue_.Pop(&raw_frame));
+    ASSERT_TRUE(raw_frame != NULL);
+    scoped_ptr<net::SpdyFrame> frame(raw_frame);
+    EXPECT_THAT(*frame, AllOf(IsControlFrameOfType(net::HEADERS),
+                              StreamIdIs(stream_id),
+                              UncompressedHeadersAre(headers),
+                              FlagFinIs(flag_fin)));
+  }
+
+  void ExpectDataFrame(net::SpdyStreamId stream_id, base::StringPiece data,
+                       bool flag_fin) {
+    net::SpdyFrame* raw_frame = NULL;
+    ASSERT_TRUE(output_queue_.Pop(&raw_frame));
+    ASSERT_TRUE(raw_frame != NULL);
+    scoped_ptr<net::SpdyFrame> frame(raw_frame);
+    EXPECT_THAT(*frame, AllOf(IsDataFrameWith(data), StreamIdIs(stream_id),
+                              FlagFinIs(flag_fin)));
+  }
+
+  void ExpectOutputQueueEmpty() {
+    net::SpdyFrame* 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_;
+  net::SpdyFramer framer_;
+  net::BufferedSpdyFramer buffered_framer_;
+  mod_spdy::SpdyFramePriorityQueue output_queue_;
+  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 = framer_.GetHighestPriority();
+  mod_spdy::SpdyStream stream(
+      spdy_version_, stream_id, associated_stream_id,
+      initial_server_push_depth, priority, net::kSpdyStreamInitialWindowSize,
+      &output_queue_, &buffered_framer_, &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 = framer_.GetHighestPriority();
+  mod_spdy::SpdyStream stream(
+      spdy_version_, stream_id, associated_stream_id,
+      initial_server_push_depth, priority, net::kSpdyStreamInitialWindowSize,
+      &output_queue_, &buffered_framer_, &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 = framer_.GetHighestPriority();
+  mod_spdy::SpdyStream stream(
+      spdy_version_, stream_id, associated_stream_id,
+      initial_server_push_depth, priority, net::kSpdyStreamInitialWindowSize,
+      &output_queue_, &buffered_framer_, &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 = framer_.GetHighestPriority();
+  mod_spdy::SpdyStream stream(
+      spdy_version_, stream_id, associated_stream_id,
+      initial_server_push_depth, priority, net::kSpdyStreamInitialWindowSize,
+      &output_queue_, &buffered_framer_, &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 = framer_.GetLowestPriority();
+  mod_spdy::SpdyStream stream(
+      spdy_version_, stream_id, associated_stream_id,
+      initial_server_push_depth, priority, net::kSpdyStreamInitialWindowSize,
+      &output_queue_, &buffered_framer_, &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 = framer_.GetHighestPriority();
+  mod_spdy::SpdyStream stream(
+      spdy_version_, stream_id, associated_stream_id,
+      initial_server_push_depth, priority, net::kSpdyStreamInitialWindowSize,
+      &output_queue_, &buffered_framer_, &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 = framer_.GetHighestPriority();
+  mod_spdy::SpdyStream stream(
+      spdy_version_, stream_id, associated_stream_id,
+      initial_server_push_depth, priority, net::kSpdyStreamInitialWindowSize,
+      &output_queue_, &buffered_framer_, &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 both SPDY v2 and SPDY v3.
+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/trunk/mod_spdy/apache/filters/server_push_filter.cc
URL: http://svn.apache.org/viewvc/httpd/mod_spdy/trunk/mod_spdy/apache/filters/server_push_filter.cc?rev=1591622&view=auto
==============================================================================
--- httpd/mod_spdy/trunk/mod_spdy/apache/filters/server_push_filter.cc (added)
+++ httpd/mod_spdy/trunk/mod_spdy/apache/filters/server_push_filter.cc Thu May  1 11:43:36 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/string_number_conversions.h"  // for StringToUint
+#include "base/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/trunk/mod_spdy/apache/filters/server_push_filter.h
URL: http://svn.apache.org/viewvc/httpd/mod_spdy/trunk/mod_spdy/apache/filters/server_push_filter.h?rev=1591622&view=auto
==============================================================================
--- httpd/mod_spdy/trunk/mod_spdy/apache/filters/server_push_filter.h (added)
+++ httpd/mod_spdy/trunk/mod_spdy/apache/filters/server_push_filter.h Thu May  1 11:43:36 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/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/trunk/mod_spdy/apache/filters/server_push_filter.h
------------------------------------------------------------------------------
    svn:eol-style = native

Added: httpd/mod_spdy/trunk/mod_spdy/apache/filters/server_push_filter_test.cc
URL: http://svn.apache.org/viewvc/httpd/mod_spdy/trunk/mod_spdy/apache/filters/server_push_filter_test.cc?rev=1591622&view=auto
==============================================================================
--- httpd/mod_spdy/trunk/mod_spdy/apache/filters/server_push_filter_test.cc (added)
+++ httpd/mod_spdy/trunk/mod_spdy/apache/filters/server_push_filter_test.cc Thu May  1 11:43:36 2014
@@ -0,0 +1,490 @@
+// 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/string_piece.h"
+#include "mod_spdy/apache/pool_util.h"
+#include "mod_spdy/common/protocol_util.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/buffered_spdy_framer.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()),
+        framer_(mod_spdy::SpdyVersionToFramerVersion(spdy_version_)),
+        buffered_framer_(mod_spdy::SpdyVersionToFramerVersion(spdy_version_)),
+        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_;
+  net::SpdyFramer framer_;
+  net::BufferedSpdyFramer buffered_framer_;
+  mod_spdy::SpdyFramePriorityQueue output_queue_;
+  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_, &buffered_framer_, &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_, &buffered_framer_, &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_, &buffered_framer_, &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_, &buffered_framer_, &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_, &buffered_framer_, &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_, &buffered_framer_, &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_, &buffered_framer_, &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_, &buffered_framer_, &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_, &buffered_framer_, &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_, &buffered_framer_, &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