text: /*
   * Licensed to the Apache Software Foundation (ASF) under one
   * or more contributor license agreements.  See the NOTICE file
   * distributed with this work for additional information
   * regarding copyright ownership.  The ASF licenses this file
   * to you under the Apache License, Version 2.0 (the
   * "License"); you may not use this file except in compliance
   * with the License.  You may obtain a copy of the License at
   * 
   *   http://www.apache.org/licenses/LICENSE-2.0
   * 
   * Unless required by applicable law or agreed to in writing,
   * software distributed under the License is distributed on an
   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   * KIND, either express or implied.  See the License for the
   * specific language governing permissions and limitations
   * under the License.
   */
  
  
  #include "pagespeed/apache/instaweb_handler.h"
  
  #include <cstddef>
  
  #include "base/logging.h"
  #include "net/instaweb/http/public/cache_url_async_fetcher.h"
  #include "net/instaweb/http/public/request_context.h"
  #include "net/instaweb/http/public/sync_fetcher_adapter_callback.h"
  #include "net/instaweb/public/global_constants.h"
  #include "net/instaweb/rewriter/public/domain_lawyer.h"
  #include "net/instaweb/rewriter/public/resource_fetch.h"
  #include "net/instaweb/rewriter/public/rewrite_driver.h"
  #include "net/instaweb/rewriter/public/rewrite_options.h"
  #include "net/instaweb/rewriter/public/rewrite_query.h"
  #include "net/instaweb/rewriter/public/rewrite_stats.h"
  #include "net/instaweb/rewriter/public/static_asset_manager.h"
  #include "pagespeed/apache/apache_config.h"
  #include "pagespeed/apache/apache_fetch.h"
  #include "pagespeed/apache/apache_message_handler.h"
  #include "pagespeed/apache/apache_request_context.h"
  #include "pagespeed/apache/apache_rewrite_driver_factory.h"
  #include "pagespeed/apache/apache_server_context.h"
  #include "pagespeed/apache/apache_writer.h"
  #include "pagespeed/apache/apr_timer.h"
  #include "pagespeed/apache/header_util.h"
  #include "pagespeed/apache/instaweb_context.h"
  #include "pagespeed/apache/mod_instaweb.h"
  #include "pagespeed/apache/simple_buffered_apache_fetch.h"
  #include "pagespeed/automatic/proxy_fetch.h"
  #include "pagespeed/automatic/proxy_interface.h"
  #include "pagespeed/kernel/base/basictypes.h"
  #include "pagespeed/kernel/base/escaping.h"
  #include "pagespeed/kernel/base/message_handler.h"
  #include "pagespeed/kernel/base/ref_counted_ptr.h"
  #include "pagespeed/kernel/base/statistics.h"
  #include "pagespeed/kernel/base/string_writer.h"
  #include "pagespeed/kernel/base/thread_system.h"
  #include "pagespeed/kernel/base/timer.h"
  #include "pagespeed/kernel/http/http_names.h"
  #include "pagespeed/kernel/http/http_options.h"
  #include "pagespeed/kernel/http/request_headers.h"
  #include "pagespeed/kernel/http/response_headers.h"
  #include "pagespeed/system/admin_site.h"
  #include "pagespeed/system/in_place_resource_recorder.h"
  
  #include "http_config.h"
  #include "http_core.h"
  #include "http_protocol.h"
  #include "http_request.h"
  #include "util_filter.h"
  #include "pagespeed/apache/apache_logging_includes.h"
  
  namespace net_instaweb {
  
  namespace {
  
  const char kAdminHandler[] = "pagespeed_admin";
  const char kGlobalAdminHandler[] = "pagespeed_global_admin";
  const char kStatisticsHandler[] = "mod_pagespeed_statistics";
  const char kConsoleHandler[] = "pagespeed_console";
  const char kGlobalStatisticsHandler[] = "mod_pagespeed_global_statistics";
  const char kMessageHandler[] = "mod_pagespeed_message";
  const char kLogRequestHeadersHandler[] = "mod_pagespeed_log_request_headers";
  const char kGenerateResponseWithOptionsHandler[] =
      "mod_pagespeed_response_options_handler";
  const char kResourceUrlNote[] = "mod_pagespeed_resource";
  const char kResourceUrlNo[] = "<NO>";
  const char kResourceUrlYes[] = "<YES>";
  
  // Set the maximum size we allow for processing a POST body. The limit of 128k
  // is based on a best guess for the maximum size of beacons required for
  // critical CSS.
  // TODO(jud): Factor this out, potentially into an option, and pass the value to
  // any filters using beacons with POST requests (CriticalImagesBeaconFilter for
  // instance).
  const size_t kMaxPostSizeBytes = 131072;
  
  }  // namespace
  
  InstawebHandler::InstawebHandler(request_rec* request)
      : request_(request),
        server_context_(InstawebContext::ServerContextFromServerRec(
            request->server)),
        rewrite_driver_(NULL),
        driver_owned_(true),
        num_response_attributes_(0),
        fetch_(NULL) {
    apache_request_context_ = server_context_->NewApacheRequestContext(request);
    request_context_.reset(apache_request_context_);
  
    // Global options
    options_ = server_context_->global_config();
  
    request_headers_.reset(new RequestHeaders);
    ApacheRequestToRequestHeaders(*request, request_headers_.get());
  
    original_url_ = InstawebContext::MakeRequestUrl(*options_, request);
    apache_request_context_->set_url(original_url_);
  
    // Note: request_context_ must be initialized before ComputeCustomOptions().
    ComputeCustomOptions();
    request_context_->set_options(options_->ComputeHttpOptions());
  }
  
  InstawebHandler::~InstawebHandler() {
    // If fetch_ is null we either never tried to fetch anything or it took
    // ownership of itself after timing out.
    if (fetch_ != NULL) {
      WaitForFetch();
      delete fetch_;
    }
    if (driver_owned_ && rewrite_driver_ != nullptr) {
      rewrite_driver_->Cleanup();
      rewrite_driver_ = nullptr;
    }
  }
  
  void InstawebHandler::WaitForFetch() {
    if (fetch_ == NULL) {
      return;  // Nothing to wait for.
    }
    fetch_->Wait();
  }
  
  void InstawebHandler::DisownDriver() {
    DCHECK(rewrite_driver_ != nullptr);
    driver_owned_ = false;
  }
  
  // Makes a driver from the request_context and options.  Note that
  // this can only be called once, as it potentially mutates the options
  // as it transfers ownership of custom_options.
  RewriteDriver* InstawebHandler::MakeDriver() {
    CHECK(fetch_ == NULL) << "Call MakeDriver before MakeFetch";
    DCHECK(rewrite_driver_ == NULL)
        << "We can only call MakeDriver once per InstawebHandler:"
        << original_url_;
  
    rewrite_driver_ = ResourceFetch::GetDriver(
        stripped_gurl_, custom_options_.release(), server_context_,
        request_context_);
  
    // If there were custom options, the ownership of the memory has
    // now been transferred to the driver, but options_ still points
    // to the same object, so it can still be used as long as the
    // driver is alive.  However, for Karma, and in case some other
    // option-merging is added to the driver someday, let's use the
    // pointer from the driver now.
    options_ = ApacheConfig::DynamicCast(rewrite_driver_->options());
    return rewrite_driver_;
  }
  
  ApacheFetch* InstawebHandler::MakeFetch(const GoogleString& url,
                                          bool buffered,
                                          StringPiece debug_info) {
    DCHECK(fetch_ == NULL);
    // ApacheFetch takes ownership of request_headers.
    RequestHeaders* request_headers = new RequestHeaders();
    ApacheRequestToRequestHeaders(*request_, request_headers);
    ApacheWriter* writer = new ApacheWriter(request_,
                                            server_context_->thread_system());
    if (rewrite_driver_ == NULL) {
      MakeDriver();
    }
    fetch_ = new ApacheFetch(
          url, debug_info, rewrite_driver_, writer, request_headers,
          request_context_, options_, server_context_->message_handler());
    fetch_->set_buffered(buffered || options_->force_buffering());
    return fetch_;
  }
  
  /* static */
  bool InstawebHandler::IsCompressibleContentType(const char* content_type) {
    if (content_type == NULL) {
      return false;
    }
    GoogleString type = content_type;
    size_t separator_idx = type.find(";");
    if (separator_idx != GoogleString::npos) {
      type.erase(separator_idx);
    }
  
    bool res = false;
    if (type.find("text/") == 0) {
      res = true;
    } else if (type.find("application/") == 0) {
      if (type.find("javascript") != type.npos ||
          type.find("json") != type.npos ||
          type.find("ecmascript") != type.npos ||
          type == "application/livescript" ||
          type == "application/js" ||
          type == "application/jscript" ||
          type == "application/x-js" ||
          type == "application/xhtml+xml" ||
          type == "application/xml") {
        res = true;
      }
    }
  
    return res;
  }
  
  /* static */
  void InstawebHandler::send_out_headers_and_body(
      request_rec* request,
      const ResponseHeaders& response_headers,
      const GoogleString& output) {
    // We always disable downstream header filters when sending out
    // pagespeed resources, since we've captured them in the origin fetch.
    ResponseHeadersToApacheRequest(response_headers, request);
    request->status = response_headers.status_code();
    DisableDownstreamHeaderFilters(request);
    if (response_headers.status_code() == HttpStatus::kOK &&
        IsCompressibleContentType(request->content_type)) {
      // Make sure compression is enabled for this response.
      ap_add_output_filter("DEFLATE", NULL, request, request->connection);
    }
  
    // Recompute the content-length, because the content may have changed.
    ap_set_content_length(request, output.size());
    // Send the body
    ap_rwrite(output.c_str(), output.size(), request);
  }
  
  // Evaluate custom_options based upon global_options, directory-specific
  // options and query-param/request-header options. Stores computed options
  // in custom_options_ if needed.  Sets options_ to point to the correct
  // options to use.
  void InstawebHandler::ComputeCustomOptions() {
    // Set directory specific options.  These will be the options for the
    // directory the resource is in, which under some configurations will be
    // different from the options for the directory that the referencing html is
    // in.  This can lead to us using different options here when regenerating
    // the resource than would be used if the resource were generated as part of
    // a rewrite kicked off by a request for the referencing html file.  This is
    // hard to fix, so instead we're documenting that you must make sure the
    // configuration for your resources matches the configuration for your html
    // files.
    {
      // In subscope so directory_options can't be used later by mistake since
      // it should only be used for computing the custom options.
      ApacheConfig* directory_options = static_cast<ApacheConfig*>
          ap_get_module_config(request_->per_dir_config, &pagespeed_module);
      if ((directory_options != NULL) && directory_options->modified()) {
        custom_options_.reset(
            server_context_->apache_factory()->NewRewriteOptions());
        custom_options_->Merge(*options_);
        directory_options->Freeze();
        custom_options_->Merge(*directory_options);
      }
    }
  
    // TODO(sligocki): Move inside PSOL.
    // Merge in query-param or header-based options.
    // Note: We do not generally get response headers in the resource flow,
    // so NULL is passed in instead.
    stripped_gurl_.Reset(original_url_);
  
    // Note: options is not actually the final options for this request, but the
    // final options depend upon the ResponseHeaders, so these are the best we
    // have. As long as we don't allow changing implicit cache TTL in
    // ResponseHeaders, this should be fine.
    const RewriteOptions* directory_aware_options =
        (custom_options_.get() != NULL) ? custom_options_.get() : options_;
    response_headers_.reset(
        new ResponseHeaders(directory_aware_options->ComputeHttpOptions()));
  
    // Copy headers_out and err_headers_out into response_headers.
    // Note that err_headers_out will come after the headers_out in the list of
    // headers. Because of this, err_headers_out will effectively override
    // headers_out when we call GetQueryOptions as it applies the header options
    // in order.
    ApacheRequestToResponseHeaders(*request_, response_headers_.get(),
                                   response_headers_.get());
    num_response_attributes_ = response_headers_->NumAttributes();
  
    // Get the remote configuration options before GetQueryOptions, as the query
    // options should override the remote config.
    if (!directory_aware_options->remote_configuration_url().empty()) {
      scoped_ptr<RewriteOptions> remote_options(directory_aware_options->Clone());
  
      server_context_->GetRemoteOptions(remote_options.get(), false);
      if (custom_options_.get() == NULL) {
        custom_options_.reset(
            server_context_->apache_factory()->NewRewriteOptions());
        custom_options_->Merge(*options_);
      }
      custom_options_->Merge(*remote_options);
    }
  
    if (!server_context_->GetQueryOptions(request_context(),
                                          directory_aware_options,
                                          &stripped_gurl_, request_headers_.get(),
                                          response_headers_.get(),
                                          &rewrite_query_)) {
      server_context_->message_handler()->Message(
          kWarning, "Invalid PageSpeed query params or headers for "
          "request %s. Serving with default options.",
          stripped_gurl_.spec_c_str());
    }
    const RewriteOptions* query_options = rewrite_query_.options();
    if (query_options != NULL) {
      if (custom_options_.get() == NULL) {
        custom_options_.reset(
            server_context_->apache_factory()->NewRewriteOptions());
        custom_options_->Merge(*options_);
      }
      custom_options_->Merge(*query_options);
      // Don't run any experiments if we're handling a customized request, unless
      // EnrollExperiment is on.
      if (!custom_options_->enroll_experiment()) {
        custom_options_->set_running_experiment(false);
      }
    }
    if (custom_options_.get() != NULL) {
      options_ = custom_options_.get();
    }
  }
  
  void InstawebHandler::RemoveStrippedResponseHeadersFromApacheRequest() {
    // Write back the modified response headers if any have been stripped by
    // GetQueryOptions (which indicates that options were found).
    // Note: GetQueryOptions should not add or mutate headers, only remove
    // them.
    DCHECK(response_headers_->NumAttributes() <= num_response_attributes_);
    if (response_headers_->NumAttributes() < num_response_attributes_) {
      // Something was stripped, but we don't know if it came from
      // headers_out or err_headers_out.  We need to treat them separately.
      if (apr_is_empty_table(request_->err_headers_out)) {
        // We know that response_headers were all from request->headers_out
        apr_table_clear(request_->headers_out);
        ResponseHeadersToApacheRequest(*response_headers_, request_);
      } else if (apr_is_empty_table(request_->headers_out)) {
        // We know that response_headers were all from err_headers_out
        apr_table_clear(request_->err_headers_out);
        ErrorHeadersToApacheRequest(*response_headers_, request_);
      } else {
        // We don't know which table changed, so scan them individually and
        // write them both back. This should be a rare case and could be
        // optimized a bit if we find that we're spending time here.
        ResponseHeaders tmp_err_resp_headers(options_->ComputeHttpOptions());
        ResponseHeaders tmp_resp_headers(options_->ComputeHttpOptions());
        ThreadSystem* thread_system = server_context_->thread_system();
        scoped_ptr<ApacheConfig> unused_opts1(
            new ApacheConfig("unused_options1", thread_system));
        scoped_ptr<ApacheConfig> unused_opts2(
            new ApacheConfig("unused_options2", thread_system));
  
        ApacheRequestToResponseHeaders(*request_, &tmp_resp_headers,
                                       &tmp_err_resp_headers);
  
        // Use ScanHeader's parsing logic to find and strip the PageSpeed
        // options from the headers. Use NULL for device_properties as no
        // device property information is needed for the stripping.
        RequestContextPtr null_request_context;
        RewriteQuery::ScanHeader(true /* enable options */,
                                 "" /* request option override */,
                                 null_request_context,
                                 &tmp_err_resp_headers,
                                 NULL /* device_properties */,
                                 unused_opts1.get(),
                                 server_context_->message_handler());
        RewriteQuery::ScanHeader(true /* enable options */,
                                 "" /* request option override */,
                                 null_request_context,
                                 &tmp_resp_headers,
                                 NULL /* device_properties */,
                                 unused_opts2.get(),
                                 server_context_->message_handler());
  
        // Write the stripped headers back to the Apache record.
        apr_table_clear(request_->err_headers_out);
        apr_table_clear(request_->headers_out);
        ResponseHeadersToApacheRequest(tmp_resp_headers, request_);
        ErrorHeadersToApacheRequest(tmp_err_resp_headers, request_);
        // Note that the ordering here matches the comment above the
        // call to ApacheRequestToResponseHeaders in
        // ComputeCustomOptions.
      }
    }
  }
  
  // Handle url as .pagespeed. rewritten resource.
  void InstawebHandler::HandleAsPagespeedResource() {
    RewriteDriver* driver = MakeDriver();
    DisownDriver();
    GoogleString output;  // TODO(jmarantz): Quit buffering resource output.
    StringWriter writer(&output);
  
    SyncFetcherAdapterCallback* callback = new SyncFetcherAdapterCallback(
        server_context_->thread_system(), &writer, request_context_);
    callback->SetRequestHeadersTakingOwnership(request_headers_.release());
  
    if (ResourceFetch::BlockingFetch(stripped_gurl_, server_context_, driver,
                                     callback)) {
      ResponseHeaders* response_headers = callback->response_headers();
      // TODO(sligocki): Check that this is already done in ResourceFetch
      // and remove redundant setting here.
      response_headers->SetDate(server_context_->timer()->NowMs());
      // ResourceFetch adds X-Page-Speed header, old mod_pagespeed code
      // did not. For now, we remove that header for consistency.
      // TODO(sligocki): Consistently use X- headers in MPS and PSOL.
      // I think it would be good to change X-Mod-Pagespeed -> X-Page-Speed
      // and use that for all HTML and resource requests.
      response_headers->RemoveAll(kPageSpeedHeader);
      send_out_headers_and_body(request_, *response_headers, output);
    } else {
      server_context_->ReportResourceNotFound(original_url_, request_);
    }
  
    callback->Release();
  }
  
  static apr_status_t DeleteInPlaceRecorder(void* object) {
    InPlaceResourceRecorder* recorded =
        static_cast<InPlaceResourceRecorder*>(object);
    delete recorded;
    return APR_SUCCESS;
  }
  
  // Handle url with In Place Resource Optimization (IPRO) flow.
  bool InstawebHandler::HandleAsInPlace() {
    bool handled = false;
  
    // We need to see if the origin request has cookies, so examine the
    // Apache request directly, as request_headers_ has been stripped of
    // headers we don't want to transmit for resource fetches.
    //
    // Note that apr_table_get is case insensitive. See
    // http://apr.apache.org/docs/apr/2.0/group__apr__tables.html#ga4db13e3915c6b9a3142b175d4c15d915
    RequestHeaders::Properties request_properties(
        apr_table_get(request_->headers_in, HttpAttributes::kCookie) != NULL,
        apr_table_get(request_->headers_in, HttpAttributes::kCookie2) != NULL,
        (apr_table_get(request_->headers_in, HttpAttributes::kAuthorization)
         != NULL) || (request_->user != NULL));
  
    RewriteDriver* driver = MakeDriver();
    MakeFetch(false /* not buffered */, "ipro");
    fetch_->set_handle_error(false);
  
    DisownDriver();
    driver->FetchInPlaceResource(stripped_gurl_, false /* proxy_mode */, fetch_);
    WaitForFetch();
    if (fetch_->status_ok()) {
      server_context_->rewrite_stats()->ipro_served()->Add(1);
      handled = true;
    } else if ((fetch_->response_headers()->status_code() ==
                CacheUrlAsyncFetcher::kNotInCacheStatus) &&
               !request_->header_only) {
      server_context_->rewrite_stats()->ipro_not_in_cache()->Add(1);
      // This URL was not found in cache (neither the input resource nor
      // a ResourceNotCacheable entry) so we need to get it into cache
      // (or at least a note that it cannot be cached stored there).
      // We do that using an Apache output filter.
      //
      // We use stripped_gurl_.Spec() rather than 'original_url_' for
      // InPlaceResourceRecorder as we want any ?ModPagespeed query-params to
      // be stripped from the cache key before we store the result in HTTPCache.
      InPlaceResourceRecorder* recorder = new InPlaceResourceRecorder(
          request_context_,
          stripped_gurl_.Spec(),
          driver->CacheFragment(),
          request_properties,
          options_->ipro_max_response_bytes(),
          options_->ipro_max_concurrent_recordings(),
          server_context_->http_cache(),
          server_context_->statistics(),
          server_context_->message_handler());
      // See mod_instaweb.cc:mod_pagespeed_register_hooks for why we need all
      // three filters.
      ap_add_output_filter(kModPagespeedInPlaceFilterName, recorder,
                           request_, request_->connection);
      ap_add_output_filter(kModPagespeedInPlaceFixHeadersName, recorder,
                           request_, request_->connection);
      ap_add_output_filter(kModPagespeedInPlaceCheckHeadersName, recorder,
                           request_, request_->connection);
      // Add a contingency cleanup path in case some module munches
      // (or doesn't produce at all) an EOS bucket. If everything
      // goes well, we will just remove it befoe cleaning up ourselves.
      apr_pool_cleanup_register(
          request_->pool, recorder, DeleteInPlaceRecorder, apr_pool_cleanup_null);
    } else {
      server_context_->rewrite_stats()->ipro_not_rewritable()->Add(1);
    }
    driver->Cleanup();
  
    return handled;
  }
  
  bool InstawebHandler::HandleAsProxy() {
    // Consider Issue 609: proxying an external CSS file via MapProxyDomain, and
    // the CSS file makes reference to a font file, which mod_pagespeed does not
    // know anything about, and does not know how to absolutify.  We need to
    // handle the request for the external font file here, even if IPRO (in place
    // resource optimization) is off.
    bool is_proxy = false;
    GoogleString mapped_url;
    GoogleString host_header;
    if (options_->domain_lawyer()->MapOriginUrl(stripped_gurl_, &mapped_url,
                                                &host_header, &is_proxy) &&
        is_proxy) {
      // TODO(jmarantz): make this unbuffered, verifying that it will
      // only call back to apache on the request thread.
      RewriteDriver* driver = MakeDriver();
      MakeFetch(mapped_url, true /* buffered */, "proxy");
      fetch_->set_is_proxy(true);
      DisownDriver();
      server_context_->proxy_fetch_factory()->StartNewProxyFetch(
          mapped_url, fetch_, driver, NULL, NULL);
      WaitForFetch();
      return true;  // handled
    }
    return false;  // declined
  }
  
  void InstawebHandler::HandleAsProxyForAll() {
    static const char kLoopValue[] = "MPS";
  
    // Note: we can't use MakeFetch here as we want ProxyInterface to create the
    // RewriteDriver.
    std::unique_ptr<RequestHeaders> request_headers(new RequestHeaders());
    ApacheRequestToRequestHeaders(*request_, request_headers.get());
  
    // Do loop detection.
    if (request_headers->HasValue(HttpAttributes::kXPageSpeedLoop, kLoopValue)) {
      write_handler_response("Loop detected on fetch in ProxyAllRequests mode; "
                             "you may need to authorize more domains. ",
                             request_);
      return;
    }
    request_headers->Add(HttpAttributes::kXPageSpeedLoop, kLoopValue);
    SimpleBufferedApacheFetch fetch(request_context_,
                                    request_headers.release(),
                                    server_context_->thread_system(),
                                    request_,
                                    server_context_->message_handler());
  
    ProxyInterface proxy_interface(
        ApacheServerContext::kProxyInterfaceStatsPrefix,
        apache_request_context_->local_ip(),
        apache_request_context_->local_port(),
        server_context_,
        server_context_->statistics());
    proxy_interface.Fetch(original_url_, server_context_->message_handler(),
                          &fetch);
  
    fetch.Wait();
  }
  
  // Determines whether the url can be handled as a mod_pagespeed or in-place
  // optimized resource, and handles it, returning true.  Success status is
  // written to the status code in the response headers.
  /* static */
  bool InstawebHandler::handle_as_resource(ApacheServerContext* server_context,
                                           request_rec* request,
                                           GoogleUrl* gurl) {
    if (!gurl->IsWebValid()) {
      return false;
    }
  
    InstawebHandler instaweb_handler(request);
    scoped_ptr<RequestHeaders> request_headers(new RequestHeaders);
    const RewriteOptions* options = instaweb_handler.options();
  
    // Finally, do the actual handling.
    bool handled = false;
    if (server_context->IsPagespeedResource(*gurl)) {
      handled = true;
      instaweb_handler.HandleAsPagespeedResource();
    } else if (instaweb_handler.HandleAsProxy()) {
      handled = true;
    } else if (options->in_place_rewriting_enabled() && options->enabled() &&
               options->IsAllowed(gurl->Spec())) {
      handled = instaweb_handler.HandleAsInPlace();
    }
  
    return handled;
  }
  
  // Write response headers and send out headers and output, including the option
  // for a custom Content-Type.
  //
  // TODO(jmarantz): consider deleting this helper method putting all responses
  // through ApacheFetch.
  /* static */
  void InstawebHandler::write_handler_response(const StringPiece& output,
                                               request_rec* request,
                                               ContentType content_type,
                                               const StringPiece& cache_control) {
    // We don't need custom options for our produced resources. In fact, options
    // shouldn't matter.
    ResponseHeaders response_headers(kDeprecatedDefaultHttpOptions);
    response_headers.SetStatusAndReason(HttpStatus::kOK);
    response_headers.set_major_version(1);
    response_headers.set_minor_version(1);
  
    response_headers.Add(HttpAttributes::kContentType, content_type.mime_type());
    // http://msdn.microsoft.com/en-us/library/ie/gg622941(v=vs.85).aspx
    // Script and styleSheet elements will reject responses with
    // incorrect MIME types if the server sends the response header
    // "X-Content-Type-Options: nosniff". This is a security feature
    // that helps prevent attacks based on MIME-type confusion.
    response_headers.Add(HttpAttributes::kXContentTypeOptions,
                         HttpAttributes::kNosniff);
    AprTimer timer;
    int64 now_ms = timer.NowMs();
    response_headers.SetDate(now_ms);
    response_headers.SetLastModified(now_ms);
    response_headers.Add(HttpAttributes::kCacheControl, cache_control);
    send_out_headers_and_body(request, response_headers, output.as_string());
  }
  
  /* static */
  void InstawebHandler::write_handler_response(const StringPiece& output,
                                               request_rec* request) {
    write_handler_response(output, request,
                           kContentTypeHtml, HttpAttributes::kNoCacheMaxAge0);
  }
  
  // Returns request URL if it was a .pagespeed. rewritten resource URL.
  // Otherwise returns NULL. Since other Apache modules can change request->uri,
  // we stow the original request URL in a note. This method reads that note
  // and thus should return the URL that the browser actually requested (rather
  // than a mod_rewrite altered URL).
  /* static */
  const char* InstawebHandler::get_instaweb_resource_url(
      request_rec* request, ApacheServerContext* server_context) {
    const char* resource = apr_table_get(request->notes, kResourceUrlNote);
  
    // If our translate_name hook, save_url_hook, failed to run because some
    // other module's translate_hook returned OK first, then run it now. The
    // main reason we try to do this early is to save our URL before mod_rewrite
    // mutates it.
    if (resource == NULL) {
      InstawebHandler::save_url_in_note(request, server_context);
      resource = apr_table_get(request->notes, kResourceUrlNote);
    }
  
    if (resource != NULL && strcmp(resource, kResourceUrlNo) == 0) {
      return NULL;
    }
  
    const char* url = apr_table_get(request->notes, kPagespeedOriginalUrl);
    return url;
  }
  
  namespace {
  
  // Used by log_request_headers for testing only.
  struct HeaderLoggingData {
    HeaderLoggingData(StringWriter* writer_in, MessageHandler* handler_in)
        : writer(writer_in), handler(handler_in) {}
    StringWriter* writer;
    MessageHandler* handler;
  };
  
  }  // namespace
  
  // Helper function to support the LogRequestHeadersHandler.  Called once for
  // each header to write header data in a form suitable for javascript inlining.
  // Used only for tests.
  /* static */
  int InstawebHandler::log_request_headers(void* logging_data,
                                           const char* key, const char* value) {
    HeaderLoggingData* hld = static_cast<HeaderLoggingData*>(logging_data);
    StringWriter* writer = hld->writer;
    MessageHandler* handler = hld->handler;
  
    GoogleString escaped_key;
    GoogleString escaped_value;
  
    EscapeToJsStringLiteral(key, false, &escaped_key);
    EscapeToJsStringLiteral(value, false, &escaped_value);
  
    writer->Write("alert(\"", handler);
    writer->Write(escaped_key, handler);
    writer->Write("=", handler);
    writer->Write(escaped_value, handler);
    writer->Write("\");\n", handler);
  
    return 1;  // Continue iteration.
  }
  
  /* static */
  void InstawebHandler::instaweb_static_handler(
      request_rec* request, ApacheServerContext* server_context) {
    StaticAssetManager* static_asset_manager =
        server_context->static_asset_manager();
    StringPiece request_uri_path = request->parsed_uri.path;
    // Strip out the common prefix url before sending to StaticAssetManager.
    StringPiece file_name = request_uri_path.substr(
        server_context->apache_factory()->static_asset_prefix().length());
    StringPiece file_contents;
    StringPiece cache_header;
    ContentType content_type;
    if (static_asset_manager->GetAsset(
        file_name, &file_contents, &content_type, &cache_header)) {
      write_handler_response(file_contents, request, content_type, cache_header);
    } else {
      server_context->ReportResourceNotFound(request->parsed_uri.path, request);
    }
  }
  
  // Append the query params from a request into data. This just parses the query
  // params from a request URL. For parsing the query params from a POST body, use
  // parse_body_from_post(). Return true if successful, otherwise, returns false
  // and sets ret to the appropriate status.
  /* static */
  bool InstawebHandler::parse_query_params(const request_rec* request,
                                           GoogleString* data,
                                           apr_status_t* ret) {
    // Add a dummy host (www.example.com) to the request URL to make it absolute
    // so that GoogleUrl can be used for parsing.
    GoogleUrl base("http://www.example.com");
    GoogleUrl url(base, request->unparsed_uri);
  
    if (!url.IsWebValid() || !url.has_query()) {
      *ret = HTTP_BAD_REQUEST;
      return false;
    }
  
    url.Query().AppendToString(data);
    return true;
  }
  
  // Read the body from a POST request and append to data. Return true if
  // successful, otherwise, returns false and sets ret to the appropriate status.
  bool InstawebHandler::parse_body_from_post(const request_rec* request,
                                             GoogleString* data,
                                             apr_status_t* ret) {
    if (request->method_number != M_POST) {
      *ret = HTTP_METHOD_NOT_ALLOWED;
      return false;
    }
  
    // Verify that the request has the correct content type for a form POST
    // submission. Ideally, we could use request->content_type here, but that is
    // coming back as NULL, even when the header was set correctly.
    const char* content_type = apr_table_get(request->headers_in,
                                             HttpAttributes::kContentType);
    if (content_type == NULL) {
      *ret = HTTP_BAD_REQUEST;
      return false;
    }
    GoogleString mime_type;
    GoogleString charset;
    if (!ParseContentType(content_type, &mime_type, &charset)) {
      *ret = HTTP_BAD_REQUEST;
      return false;
    }
    // TODO(jud): Set the charset on the beacon to a known value (say, UTF-8), and
    // check here that it's as expected. Intended as a cheap-and-nasty test that
    // the beacon came from our JS and not some black hat. Easily subvertible but
    // better than nothing (?).
    if (!StringCaseEqual(mime_type, "application/x-www-form-urlencoded") &&
        !StringCaseEqual(mime_type, "multipart/form-data")) {
      *ret = HTTP_BAD_REQUEST;
      return false;
    }
  
    // Setup the number of bytes to try to read from the POST body. If the
    // Content-Length header is set, use it, otherwise try to pull up to
    // kMaxPostSizeBytes.
    int content_len = kMaxPostSizeBytes;
    const char* content_len_str = apr_table_get(request->headers_in,
                                                HttpAttributes::kContentLength);
    if (content_len_str != NULL) {
      if (!StringToInt(content_len_str, &content_len)) {
        *ret = HTTP_BAD_REQUEST;
        return false;
      }
      if (static_cast<size_t>(content_len) > kMaxPostSizeBytes) {
        *ret = HTTP_REQUEST_ENTITY_TOO_LARGE;
        return false;
      }
    }
  
    // Parse the incoming brigade and add the contents to data. In apache 2.4 we
    // could just use ap_parse_form_data. See the example at
    // http://httpd.apache.org/docs/2.4/developer/modguide.html#snippets.
    apr_bucket_brigade* bbin =
        apr_brigade_create(request->pool, request->connection->bucket_alloc);
  
    bool eos = false;
  
    while (!eos) {
      apr_status_t rv = ap_get_brigade(request->input_filters, bbin,
                                       AP_MODE_READBYTES, APR_BLOCK_READ,
                                       content_len);
      if (rv != APR_SUCCESS) {
        // Form input read failed.
        *ret = HTTP_INTERNAL_SERVER_ERROR;
        return false;
      }
      for (apr_bucket* bucket = APR_BRIGADE_FIRST(bbin);
           bucket != APR_BRIGADE_SENTINEL(bbin);
           bucket = APR_BUCKET_NEXT(bucket) ) {
        if (!APR_BUCKET_IS_METADATA(bucket)) {
          const char* buf = NULL;
          size_t bytes = 0;
          rv = apr_bucket_read(bucket, &buf, &bytes, APR_BLOCK_READ);
          if (rv != APR_SUCCESS) {
            *ret = HTTP_INTERNAL_SERVER_ERROR;
            return false;
          }
          if (data->length() + bytes > kMaxPostSizeBytes) {
            *ret = HTTP_REQUEST_ENTITY_TOO_LARGE;
            return false;
          }
          data->append(buf, bytes);
        } else if (APR_BUCKET_IS_EOS(bucket)) {
          eos = true;
          break;
        }
      }
      apr_brigade_cleanup(bbin);
    }
  
    // No need to modify ret as it is only used if reading the POST failed.
    return true;
  }
  
  /* static */
  apr_status_t InstawebHandler::instaweb_beacon_handler(
      request_rec* request, ApacheServerContext* server_context) {
    GoogleString data;
    apr_status_t ret = DECLINED;
    if (request->method_number == M_GET) {
      if (!parse_query_params(request, &data, &ret)) {
        return ret;
      }
    } else if (request->method_number == M_POST) {
      GoogleString query_param_data, post_data;
      // Even if the beacon is a POST, the originating url should be in the query
      // params, not the POST body.
      if (!parse_query_params(request, &query_param_data, &ret)) {
        return ret;
      }
      if (!parse_body_from_post(request, &post_data, &ret)) {
        return ret;
      }
      StrAppend(&data, query_param_data, "&", post_data);
    } else {
      return HTTP_METHOD_NOT_ALLOWED;
    }
    RequestContextPtr request_context(
        server_context->NewApacheRequestContext(request));
    StringPiece user_agent = apr_table_get(request->headers_in,
                                           HttpAttributes::kUserAgent);
    server_context->HandleBeacon(data, user_agent, request_context);
    apr_table_set(request->headers_out, HttpAttributes::kCacheControl,
                  HttpAttributes::kNoCacheMaxAge0);
    return HTTP_NO_CONTENT;
  }
  
  /* static */
  bool InstawebHandler::IsBeaconUrl(const RewriteOptions::BeaconUrl& beacons,
                                    const GoogleUrl& gurl) {
    // Check if the full path without query parameters equals the beacon URL,
    // either the http or https version (we're too lazy to check specifically).
    // This handles both GETs, which include query parameters, and POSTs,
    // which will only have the originating url in the query params.
    if (!gurl.IsWebValid()) {
      return false;
    }
    // Ignore query params in the beacon URLs. Normally the beacon URL won't have
    // a query param, but it could have been added using ModPagespeedBeaconUrl.
    return (gurl.PathSansQuery() == beacons.http_in ||
            gurl.PathSansQuery() == beacons.https_in);
  }
  
  /* static */
  bool InstawebHandler::is_pagespeed_subrequest(request_rec* request) {
    StringPiece user_agent = apr_table_get(request->headers_in,
                                           HttpAttributes::kUserAgent);
    return (user_agent.find(kModPagespeedSubrequestUserAgent) != user_agent.npos);
  }
  
  /* static */
  apr_status_t InstawebHandler::instaweb_handler(request_rec* request) {
    apr_status_t ret = DECLINED;
    ApacheServerContext* server_context =
        InstawebContext::ServerContextFromServerRec(request->server);
    ApacheConfig* global_config = server_context->global_config();
    // Escape ASAP if we're in unplugged mode.
    if (global_config->unplugged()) {
      return DECLINED;
    }
  
    // Flushing the cache mutates global_options, so this has to happen before we
    // construct the options that we use to decide whether IPRO is enabled.  Note
    // that the global_config might be altered by this, but the pointer will not
    // change.
    server_context->FlushCacheIfNecessary();
  
    ApacheRewriteDriverFactory* factory = server_context->apache_factory();
    ApacheMessageHandler* message_handler = factory->apache_message_handler();
    StringPiece request_handler_str = request->handler;
  
    const char* url = InstawebContext::MakeRequestUrl(*global_config, request);
    GoogleUrl gurl;
    if (url == NULL || !gurl.Reset(url)) {
      return DECLINED;  // URL not valid, let someone other module handle.
    }
  
    if (global_config->proxy_all_requests_mode() && gurl.IsWebValid()) {
      InstawebHandler instaweb_handler(request);
      // TODO(morlovich): Still export stats and the like?
      instaweb_handler.HandleAsProxyForAll();
      return APACHE_OK;
    }
  
    if (request_handler_str == kStatisticsHandler &&
        global_config->StatisticsAccessAllowed(gurl)) {
      InstawebHandler instaweb_handler(request);
      server_context->StatisticsPage(false /* not global */,
                                     instaweb_handler.query_params(),
                                     instaweb_handler.options(),
                                     instaweb_handler.MakeFetch(
                                         false /* unbuffered */, "local-stats"));
      return APACHE_OK;
    } else if (request_handler_str == kGlobalStatisticsHandler &&
               global_config->GlobalStatisticsAccessAllowed(gurl)) {
      InstawebHandler instaweb_handler(request);
      server_context->StatisticsPage(true /* global */,
                                     instaweb_handler.query_params(),
                                     instaweb_handler.options(),
                                     instaweb_handler.MakeFetch(
                                         false /* unbuffered */, "global-stats"));
      return APACHE_OK;
    } else if (request_handler_str == kAdminHandler &&
               global_config->AdminAccessAllowed(gurl)) {
      InstawebHandler instaweb_handler(request);
      // The fetch has to be buffered because if it's a cache lookup it could
      // complete asynchrously via the rewrite thread.
      server_context->AdminPage(false /* not global */,
                                instaweb_handler.stripped_gurl(),
                                instaweb_handler.query_params(),
                                instaweb_handler.options(),
                                instaweb_handler.MakeFetch(
                                    true /* buffered */, "local-admin"));
      ret = APACHE_OK;
    } else if (request_handler_str == kGlobalAdminHandler &&
               global_config->GlobalAdminAccessAllowed(gurl)) {
      InstawebHandler instaweb_handler(request);
      // The fetch has to be buffered because if it's a cache lookup it could
      // complete asynchrously via the rewrite thread.
      server_context->AdminPage(true /* global */,
                                instaweb_handler.stripped_gurl(),
                                instaweb_handler.query_params(),
                                instaweb_handler.options(),
                                instaweb_handler.MakeFetch(
                                    true /* buffered */, "global-admin"));
      ret = APACHE_OK;
    } else if (global_config->enable_cache_purge() &&
               !global_config->purge_method().empty() &&
               (global_config->purge_method() == request->method)) {
      InstawebHandler instaweb_handler(request);
      AdminSite* admin_site = server_context->admin_site();
      // I'm not convinced that the purge handler must complete synchronously.  It
      // schedules work on the rewrite driver factory's scheduler, and while in my
      // testing it processes everything on the calling thread I'm not sure this
      // is part of the contract.  The response is just headers and a few bytes of
      // body, so buffering is basically free.  To be on the safe side let's
      // buffer this one too.
      admin_site->PurgeHandler(instaweb_handler.original_url_,
                               server_context->cache_path(),
                               instaweb_handler.MakeFetch(
                                   true /* buffered */, "purge"));
      ret = APACHE_OK;
    } else if (request_handler_str == kConsoleHandler &&
               global_config->ConsoleAccessAllowed(gurl)) {
      InstawebHandler instaweb_handler(request);
      server_context->ConsoleHandler(*instaweb_handler.options(),
                                     AdminSite::kOther,
                                     instaweb_handler.query_params(),
                                     instaweb_handler.MakeFetch(
                                         false /* unbuffered */, "console"));
      ret = APACHE_OK;
    } else if (request_handler_str == kMessageHandler &&
               global_config->MessagesAccessAllowed(gurl)) {
      InstawebHandler instaweb_handler(request);
      server_context->MessageHistoryHandler(
          *instaweb_handler.options(),
          AdminSite::kOther,
          instaweb_handler.MakeFetch(false /* unbuffered */, "messages"));
      ret = APACHE_OK;
    } else if (request_handler_str == kLogRequestHeadersHandler) {
      // For testing CustomFetchHeader.
      GoogleString output;
      StringWriter writer(&output);
      HeaderLoggingData header_logging_data(&writer, message_handler);
      apr_table_do(&log_request_headers, &header_logging_data,
                   request->headers_in, NULL);
  
      write_handler_response(output, request, kContentTypeJavascript, "public");
      ret = APACHE_OK;
    } else if (strcmp(request->handler, kGenerateResponseWithOptionsHandler) == 0
               && request->uri != NULL) {
      // This handler is only needed for apache_system_test. It adds headers to
      // headers_out and/or err_headers_out to test handling of parameters in
      // those resources.
      if (strstr(request->parsed_uri.query, "headers_out") != NULL) {
        apr_table_add(request->headers_out, "PageSpeed", "off");
      } else if (strstr(request->parsed_uri.query, "headers_errout") != NULL) {
        apr_table_add(request->err_headers_out, "PageSpeed", "off");
      } else if (strstr(request->parsed_uri.query, "headers_override") != NULL) {
        apr_table_add(request->headers_out, "PageSpeed", "off");
        apr_table_add(request->headers_out, "PageSpeedFilters",
                      "-remove_comments");
        apr_table_add(request->err_headers_out, "PageSpeed", "on");
        apr_table_add(request->err_headers_out, "PageSpeedFilters",
                      "+remove_comments");
      } else if (strstr(request->parsed_uri.query, "headers_combine") != NULL) {
        apr_table_add(request->headers_out, "PageSpeed", "on");
        apr_table_add(request->err_headers_out, "PageSpeedFilters",
                      "+remove_comments");
      }
    } else {
      const char* url = InstawebContext::MakeRequestUrl(*global_config, request);
      // Do not try to rewrite our own sub-request.
      if (url != NULL) {
        GoogleUrl gurl(url);
        if (!gurl.IsWebValid()) {
          ap_log_rerror(APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, request,
                        "Ignoring invalid URL: %s", gurl.spec_c_str());
        } else if (IsBeaconUrl(global_config->beacon_url(), gurl)) {
          ret = instaweb_beacon_handler(request, server_context);
        // For the beacon accept any method; for all others only allow GETs.
        } else if (request->method_number != M_GET) {
          ap_log_rerror(APLOG_MARK, APLOG_DEBUG, APR_SUCCESS, request,
                        "Not rewriting non-GET %d of %s",
                        request->method_number, gurl.spec_c_str());
        } else if (gurl.PathSansLeaf() ==
                   server_context->apache_factory()->static_asset_prefix()) {
          instaweb_static_handler(request, server_context);
          ret = APACHE_OK;
        } else if (!is_pagespeed_subrequest(request) &&
                   handle_as_resource(server_context, request, &gurl)) {
          ret = APACHE_OK;
        }
  
        // Check for HTTP_NO_CONTENT here since that's the status used for a
        // successfully handled beacon.
        if (ret != APACHE_OK && ret != HTTP_NO_CONTENT &&
            gurl.Host() != "localhost" &&
            (global_config->slurping_enabled() || global_config->test_proxy() ||
             !global_config->domain_lawyer()->proxy_suffix().empty())) {
          // TODO(jmarantz): Consider moving the InstawebHandler up above
          // where we assign 'const char* url' above because we are repeating
          // a bunch of string-hacking here in the constructor.  However, we
          // really want the query-param evaluation happening inside the
          // constructor here.
          InstawebHandler instaweb_handler(request);
          if (instaweb_handler.ProxyUrl()) {
            ret = APACHE_OK;
          }
        }
      }
    }
    return ret;
  }
  
  // This translator must be inserted into the translate_name chain
  // prior to mod_rewrite.  By saving the original URL in a
  // request->notes and using that in our handler, we prevent
  // mod_rewrite from borking URL names that need to be handled by
  // mod_pagespeed.
  //
  // This hack seems to be the most robust way to immunize mod_pagespeed
  // from when mod_rewrite rewrites the URL.  We still need mod_rewrite
  // to do required complex processing of the filename (e.g. prepending
  // the DocumentRoot) so mod_authz_host is happy, so we return DECLINED
  // even for mod_pagespeed resources.
  //
  // One alternative strategy is to return OK to bypass mod_rewrite
  // entirely, but then we'd have to duplicate the functionality in
  // mod_rewrite that prepends the DocumentRoot, which is itself
  // complex.  See mod_rewrite.c:hook_fixup(), and look for calls to
  // ap_document_root().
  //
  // Or we could return DECLINED but set a note "mod_rewrite_rewritten"
  // to try to convince mod_rewrite to leave our URLs alone, which seems
  // fragile as that's an internal string literal in mod_rewrite.c and
  // is not documented anywhere.
  //
  // Another strategy is to return OK but leave request->filename NULL.
  // In that case, the server kernel generates an ominous 'info' message:
  //
  //     [info] [client ::1] Module bug?  Request filename is missing for URI
  //     /mod_pagespeed_statistics
  //
  // This is generated by httpd/src/server/request.c line 486, and right
  // above that is this comment:
  //
  //     "OK" as a response to a real problem is not _OK_, but to
  //     allow broken modules to proceed, we will permit the
  //     not-a-path filename to pass the following two tests.  This
  //     behavior may be revoked in future versions of Apache.  We
  //     still must catch it later if it's heading for the core
  //     handler.  Leave INFO notes here for module debugging.
  //
  // It seems like the simplest, most robust approach is to squirrel
  // away the original URL *before* mod_rewrite sees it in
  // kPagespeedOriginalUrl "mod_pagespeed_url" and use *that* rather than
  // request->unparsed_uri (which mod_rewrite might have mangled) when
  // processing the request.
  //
  // Additionally we store whether or not this request is a pagespeed
  // resource or not in kResourceUrlNote.
  /* static */
  apr_status_t InstawebHandler::save_url_hook(request_rec *request) {
    ApacheServerContext* server_context =
        InstawebContext::ServerContextFromServerRec(request->server);
    return save_url_in_note(request, server_context);
  }
  
  /* static */
  apr_status_t InstawebHandler::save_url_in_note(
      request_rec *request, ApacheServerContext* server_context) {
    // Escape ASAP if we're in unplugged mode.
    if (server_context->global_config()->unplugged()) {
      return DECLINED;
    }
  
    // This call to MakeRequestUrl() not only returns the url but also
    // saves it for future use so that if another module changes the
    // url in the request, we still have the original one.
    const char* url = InstawebContext::MakeRequestUrl(
        *server_context->global_options(), request);
    GoogleUrl gurl(url);
  
    bool bypass_mod_rewrite = false;
    if (gurl.IsWebValid()) {
      // Note: We cannot use request->handler because it may not be set yet :(
      // TODO(sligocki): Make this robust to custom statistics and beacon URLs.
      StringPiece leaf = gurl.LeafSansQuery();
      if (leaf == kStatisticsHandler || leaf == kConsoleHandler ||
          leaf == kGlobalStatisticsHandler || leaf == kMessageHandler ||
          leaf == kAdminHandler ||
          gurl.PathSansLeaf() ==
            server_context->apache_factory()->static_asset_prefix() ||
          IsBeaconUrl(server_context->global_options()->beacon_url(), gurl) ||
          server_context->IsPagespeedResource(gurl)) {
        bypass_mod_rewrite = true;
      }
    }
  
    if (bypass_mod_rewrite) {
      apr_table_set(request->notes, kResourceUrlNote, kResourceUrlYes);
    } else {
      // Leave behind a note for non-instaweb requests that says that
      // our handler got called and we decided to pass.  This gives us
      // one final chance at serving resources in the presence of a
      // module that intercepted 'translate_name' before mod_pagespeed.
      // The absence of this marker indicates that translate_name did
      // not get a chance to run, and thus we should try to look at
      // the URI directly.
      apr_table_set(request->notes, kResourceUrlNote, kResourceUrlNo);
    }
    return DECLINED;
  }
  
  // Override core_map_to_storage for pagespeed resources.
  /* static */
  apr_status_t InstawebHandler::instaweb_map_to_storage(request_rec* request) {
    if (request->proxyreq == PROXYREQ_REVERSE) {
      // If Apache is acting as a reverse proxy for this request there is no
      // point in walking the directory because it doesn't apply to this
      // server's htdocs tree, it applies to the server we are proxying to.
      // This can result in it raising a 403 because some path doesn't exist.
      // Note that experimenting shows that it doesn't matter if we return OK
      // or DECLINED here, at least with URLs that aren't overly long; also,
      // we actually fetch the DECODED URL (no .pagespeed. etc) from the proxy
      // server and we rewrite it ourselves.
      return DECLINED;
    }
  
    if (request->filename == NULL) {
      // We set filename to NULL below, and it appears other modules do too
      // (the WebSphere plugin for example; see issue 610), so to prevent a
      // dereference of NULL.
      return DECLINED;
    }
  
    ApacheServerContext* server_context =
        InstawebContext::ServerContextFromServerRec(request->server);
    if (server_context->global_config()->unplugged()) {
      // If we're in unplugged mode then none of our hooks apply so escape ASAP.
      return DECLINED;
    }
  
    if (get_instaweb_resource_url(request, server_context) == NULL) {
      return DECLINED;
    }
  
    // core_map_to_storage does at least two things:
    //  1) checks filename length limits
    //  2) determines directory specific options
    // We want (2) but not (1).  If we simply return OK we will keep
    // core_map_to_storage from running and let through our long filenames but
    // resource requests that require regeneration will not respect directory
    // specific options.
    //
    // To fix this we need to be more dependent on apache internals than we
    // would like.  core_map_to_storage always calls ap_directory_walk(request),
    // which does both (1) and (2) and appears to work entirely off of
    // request->filename.  But ap_directory_walk doesn't care whether the last
    // request->segment of the path actually exists.  So if we change the
    // request->filename from something like:
    //    /var/www/path/to/LEAF_WHICH_MAY_BE_HUGE.pagespeed.FILTER.HASH.EXT
    // to:
    //    /var/www/path/to/A
    // then we will bypass the filename length limit without harming the load of
    // directory specific options.
    //
    // So: modify request->filename in place to cut it off after the last '/'
    // character and replace the whole leaf with 'A', and then call
    // ap_directory_walk to figure out custom options.
    char* filename_starting_at_last_slash = strrchr(request->filename, '/');
    if (filename_starting_at_last_slash != NULL &&
        filename_starting_at_last_slash[1] != '\0') {
      filename_starting_at_last_slash[1] = 'A';
      filename_starting_at_last_slash[2] = '\0';
    }
    ap_directory_walk(request);
  
    // mod_speling, if enabled, looks for the filename on the file system,
    // and tries to "correct" the spelling.  This is not desired for
    // mod_pagesped resources, but mod_speling will not do this damage
    // when request->filename == NULL.  See line 219 of
    // http://svn.apache.org/viewvc/httpd/httpd/trunk/modules/mappers/
    // mod_speling.c?revision=983065&view=markup
    //
    // Note that mod_speling runs 'hook_fixups' at APR_HOOK_LAST, and
    // we are currently running instaweb_map_to_storage in map_to_storage
    // HOOK_FIRST-2, which is a couple of phases before hook_fixups.
    //
    // If at some point we stop NULLing the filename here we need to modify the
    // code above that mangles it to use a temporary buffer instead.
    request->filename = NULL;
  
    // While setting request->filename helps get mod_speling (as well as
    // mod_mime and mod_mime_magic) out of our hair, it causes crashes
    // in mod_negotiation (if on) when finfo.filetype is APR_NOFILE.
    // So we give it a type that's something other than APR_NOFILE (plus we
    // also don't want APR_DIR, since that would make mod_mime to set the
    // mimetype to httpd/unix-directory).
    request->finfo.filetype = APR_UNKFILE;
  
    // Keep core_map_to_storage from running and rejecting our long filenames.
    return APACHE_OK;
  }
  
  /* static */
  void InstawebHandler::AboutToBeDoneWithRecorder(
      request_rec* request, InPlaceResourceRecorder* recorder) {
    apr_pool_cleanup_kill(request->pool, recorder, DeleteInPlaceRecorder);
  }
  
  }  // namespace net_instaweb
  