a41cdab05e
* standby: add standby mode In standby mode PageSpeed is off, except it serves .pagespeed. resources and PageSpeed query parameters are interpreted. This is equivalent to "off" in mod_pagespeed. With this change, "off" in mod_pagespeed is deprecated, and people should use "standby" instead. * add test file * rewrite doc * get tests to pass under memcached * doc: minor display tweak
776 lines
30 KiB
C++
776 lines
30 KiB
C++
// 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 "net/instaweb/rewriter/public/rewrite_query.h"
|
|
|
|
#include <algorithm> // for std::binary_search
|
|
#include <map>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "base/logging.h"
|
|
#include "net/instaweb/http/public/request_context.h"
|
|
#include "net/instaweb/rewriter/public/image_rewrite_filter.h"
|
|
#include "net/instaweb/rewriter/public/request_properties.h"
|
|
#include "net/instaweb/rewriter/public/resource_namer.h"
|
|
#include "net/instaweb/rewriter/public/rewrite_driver.h"
|
|
#include "net/instaweb/rewriter/public/rewrite_driver_factory.h"
|
|
#include "net/instaweb/rewriter/public/rewrite_filter.h"
|
|
#include "net/instaweb/rewriter/public/rewrite_options.h"
|
|
#include "net/instaweb/rewriter/public/server_context.h"
|
|
#include "pagespeed/kernel/base/message_handler.h"
|
|
#include "pagespeed/kernel/base/scoped_ptr.h"
|
|
#include "pagespeed/kernel/base/string.h"
|
|
#include "pagespeed/kernel/base/string_multi_map.h"
|
|
#include "pagespeed/kernel/base/string_util.h"
|
|
#include "pagespeed/kernel/http/google_url.h"
|
|
#include "pagespeed/kernel/http/http_names.h"
|
|
#include "pagespeed/kernel/http/query_params.h"
|
|
#include "pagespeed/kernel/http/response_headers.h"
|
|
|
|
namespace net_instaweb {
|
|
|
|
namespace {
|
|
|
|
// We use + and = inside the resource-options URL segment because they will not
|
|
// be quoted by UrlEscaper, unlike "," and ":".
|
|
const char kResourceFilterSeparator[] = "+";
|
|
const char kResourceOptionValueSeparator[] = "=";
|
|
|
|
const char kProxyOptionSeparator[] = ",";
|
|
const char kProxyOptionValueSeparator = '=';
|
|
const char kProxyOptionVersion[] = "v";
|
|
const char kProxyOptionMode[] = "m";
|
|
const char kProxyOptionImageQualityPreference[] = "iqp";
|
|
const char kProxyOptionValidVersionValue[] = "1";
|
|
|
|
StringPiece SanitizeValueAsQP(StringPiece untrusted_value,
|
|
GoogleUrl* storage) {
|
|
// This is ever so slightly hacky: we dummy up an URL with a QP where the
|
|
// value of the QP is the untrusted value, then we discard everything
|
|
// prior to the possibly-modified value in the resulting GoogleUrl.
|
|
const char kUrlBase[] = "http://www.example.com/?x=";
|
|
storage->Reset(StrCat(kUrlBase, untrusted_value));
|
|
StringPiece sanitized_value = storage->Spec();
|
|
return StringPiece(sanitized_value.data() + STATIC_STRLEN(kUrlBase),
|
|
sanitized_value.size() - STATIC_STRLEN(kUrlBase));
|
|
}
|
|
|
|
} // namespace
|
|
|
|
const char RewriteQuery::kModPagespeed[] = "ModPagespeed";
|
|
const char RewriteQuery::kPageSpeed[] = "PageSpeed";
|
|
|
|
const char RewriteQuery::kModPagespeedFilters[] = "ModPagespeedFilters";
|
|
const char RewriteQuery::kPageSpeedFilters[] = "PageSpeedFilters";
|
|
|
|
const char RewriteQuery::kNoscriptValue[] = "noscript";
|
|
|
|
template <class HeaderT>
|
|
RewriteQuery::Status RewriteQuery::ScanHeader(
|
|
bool allow_options,
|
|
const GoogleString& request_option_override,
|
|
const RequestContextPtr& request_context,
|
|
HeaderT* headers,
|
|
RequestProperties* request_properties,
|
|
RewriteOptions* options,
|
|
MessageHandler* handler) {
|
|
Status status = kNoneFound;
|
|
|
|
if (headers == NULL) {
|
|
return status;
|
|
}
|
|
|
|
// Check to see if the override token exists.
|
|
if (!allow_options && !request_option_override.empty()) {
|
|
GoogleString mod_pagespeed_override =
|
|
StrCat(kModPagespeed, RewriteOptions::kRequestOptionOverride);
|
|
GoogleString page_speed_override =
|
|
StrCat(kPageSpeed, RewriteOptions::kRequestOptionOverride);
|
|
for (int i = 0, n = headers->NumAttributes(); i < n; ++i) {
|
|
const StringPiece name(headers->Name(i));
|
|
const GoogleString& value = headers->Value(i);
|
|
if (name == mod_pagespeed_override || name == page_speed_override) {
|
|
allow_options = (value == request_option_override);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Tracks the headers that need to be removed.
|
|
// It doesn't matter what type of headers we use, so we use RequestHeaders.
|
|
RequestHeaders headers_to_remove;
|
|
|
|
for (int i = 0, n = headers->NumAttributes(); i < n; ++i) {
|
|
const StringPiece name(headers->Name(i));
|
|
const GoogleString& value = headers->Value(i);
|
|
switch (ScanNameValue(name, value, allow_options, request_context,
|
|
request_properties, options, handler)) {
|
|
case kNoneFound:
|
|
break;
|
|
case kSuccess:
|
|
if (name.starts_with(kModPagespeed) || name.starts_with(kPageSpeed)) {
|
|
headers_to_remove.Add(name, value);
|
|
}
|
|
status = kSuccess;
|
|
break;
|
|
case kInvalid:
|
|
return kInvalid;
|
|
}
|
|
}
|
|
|
|
// TODO(bolian): jmarantz suggested below change. we should make a
|
|
// StringSetInsensitive and put all the names we want to remove including
|
|
// XPSAClientOptions and then call RemoveAllFromSet.
|
|
// That will be more efficient.
|
|
for (int i = 0, n = headers_to_remove.NumAttributes(); i < n; ++i) {
|
|
headers->Remove(headers_to_remove.Name(i), headers_to_remove.Value(i));
|
|
}
|
|
// kXPsaClientOptions is meant for proxy only. Remove it in any case.
|
|
headers->RemoveAll(HttpAttributes::kXPsaClientOptions);
|
|
|
|
return status;
|
|
}
|
|
|
|
RewriteQuery::RewriteQuery() {
|
|
}
|
|
|
|
RewriteQuery::~RewriteQuery() {
|
|
}
|
|
|
|
// Scan for option-sets in cookies, query params, request and response headers.
|
|
// We only allow a limited number of options to be set. In particular, some
|
|
// options are risky to set this way, such as image inline threshold, which
|
|
// exposes a DOS vulnerability and a risk of poisoning our internal cache, and
|
|
// domain adjustments, which can introduce a security vulnerability.
|
|
RewriteQuery::Status RewriteQuery::Scan(
|
|
bool allow_related_options,
|
|
bool allow_options_to_be_specified_by_cookies,
|
|
const GoogleString& request_option_override,
|
|
const RequestContextPtr& request_context,
|
|
RewriteDriverFactory* factory,
|
|
ServerContext* server_context,
|
|
GoogleUrl* request_url,
|
|
RequestHeaders* request_headers,
|
|
ResponseHeaders* response_headers,
|
|
MessageHandler* handler) {
|
|
Status status = kNoneFound;
|
|
query_params_.Clear();
|
|
pagespeed_query_params_.Clear();
|
|
pagespeed_option_cookies_.Clear();
|
|
options_.reset(NULL);
|
|
|
|
// To support serving resources from servers that don't share the
|
|
// same settings as the ones generating HTML, we can put whitelisted
|
|
// option-settings into the query-params by ID. But we expose this
|
|
// setting (a) only for .pagespeed. resources, not HTML, and (b)
|
|
// only when allow_related_options is true.
|
|
ResourceNamer namer;
|
|
|
|
bool return_after_parsing = false;
|
|
if (allow_related_options &&
|
|
namer.DecodeIgnoreHashAndSignature(request_url->LeafSansQuery()) &&
|
|
namer.has_options()) {
|
|
const RewriteFilter* rewrite_filter =
|
|
server_context->FindFilterForDecoding(namer.id());
|
|
if (rewrite_filter != NULL) {
|
|
options_.reset(factory->NewRewriteOptionsForQuery());
|
|
status = ParseResourceOption(namer.options(), options_.get(),
|
|
rewrite_filter);
|
|
if (status != kSuccess) {
|
|
options_.reset(NULL);
|
|
|
|
// We want query_params() to be populated after calling
|
|
// RewriteQuery::Scan, even if any URL-embedded configuration
|
|
// parameters are invalid. So we delay our early exit until
|
|
// after the query_params_.Parse call below.
|
|
return_after_parsing = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Extract all cookies iff we can use them to set options.
|
|
RequestHeaders::CookieMultimap no_cookies;
|
|
const RequestHeaders::CookieMultimap& all_cookies(
|
|
(allow_options_to_be_specified_by_cookies && request_headers != NULL)
|
|
? request_headers->GetAllCookies()
|
|
: no_cookies);
|
|
|
|
// For XmlHttpRequests, disable filters that can't run in Ajax.
|
|
//
|
|
// For example, for filters that insert scripts, there will be two
|
|
// copies of the same scripts in the html dom -- one from main html
|
|
// page and another from html content fetched from ajax --- which
|
|
// will generally confuse the heck out of it. The code for this is a
|
|
// little special since unlike a PageSpeedFoo= header we should not
|
|
// take it as an invitation to turn stuff on.
|
|
//
|
|
// Another commmon case is filters that require a <head> element -- we
|
|
// don't want to add a <head> in the ajax response, so we can't run
|
|
// any filters that might depend on that.
|
|
//
|
|
// TODO(sriharis): Set a flag in RewriteOptions indicating that we are
|
|
// working with Ajax and thus should not assume the base URL is correct.
|
|
// Note that there is no guarantee that the header will be set on an ajax
|
|
// request and so the option will not be set for all ajax requests.
|
|
//
|
|
// TODO(jmarantz): Ajax status should probably be in RequestProperties
|
|
// rather than RewriteOptions so we don't have to create a new driver
|
|
// on an ajax request, but can instead reference properties. However
|
|
// that change may be fairly significant.
|
|
if (request_headers != NULL && request_headers->IsXmlHttpRequest()) {
|
|
if (options_.get() == NULL) {
|
|
options_.reset(factory->NewRewriteOptionsForQuery());
|
|
}
|
|
options_->DisableFiltersThatCantRunInAjax();
|
|
status = kSuccess;
|
|
}
|
|
|
|
// See if anything looks even remotely like one of our options before doing
|
|
// any more work. Note that when options are correctly embedded in the URL,
|
|
// we will have a success-status here. But we still allow a hand-added
|
|
// query-param to override the embedded options.
|
|
query_params_.ParseFromUrl(*request_url);
|
|
if (return_after_parsing ||
|
|
!MayHaveCustomOptions(query_params_, request_headers, response_headers,
|
|
all_cookies)) {
|
|
return status;
|
|
}
|
|
|
|
if (options_.get() == NULL) {
|
|
options_.reset(factory->NewRewriteOptionsForQuery());
|
|
}
|
|
|
|
scoped_ptr<RequestProperties> request_properties;
|
|
if (request_headers != NULL) {
|
|
request_properties.reset(server_context->NewRequestProperties());
|
|
request_properties->SetUserAgent(
|
|
request_headers->Lookup1(HttpAttributes::kUserAgent));
|
|
}
|
|
|
|
// Check to see if options should be parsed.
|
|
// If the config disallows parsing, and the proper token is not provided,
|
|
// do not use the options passed in the url.
|
|
bool allow_options = true;
|
|
if (!request_option_override.empty()) {
|
|
allow_options = false;
|
|
GoogleString override_token;
|
|
GoogleString mod_pagespeed_override =
|
|
StrCat(kModPagespeed, RewriteOptions::kRequestOptionOverride);
|
|
GoogleString page_speed_override =
|
|
StrCat(kPageSpeed, RewriteOptions::kRequestOptionOverride);
|
|
if (query_params_.Lookup1Unescaped(mod_pagespeed_override,
|
|
&override_token) ||
|
|
query_params_.Lookup1Unescaped(page_speed_override, &override_token)) {
|
|
allow_options = (override_token == request_option_override);
|
|
}
|
|
}
|
|
|
|
// Scan for options set as cookies. They can be overridden by QPs or headers.
|
|
// An explanation of the life cycle of a PageSpeed option cookie:
|
|
// * Initially the value is passed in as a GoogleUrl query parameter.
|
|
// * GoogleUrl does minimal escaping, mainly removing whitespace and
|
|
// percent-encoding control characters.
|
|
// * That is parsed by QueryParams (above), which does no further escaping.
|
|
// * Since cookie values have restrictions on the characters allowed in the
|
|
// value (e.g. no ';'s), we GoogleUrl::Escape the value, which does
|
|
// significant percent-escaping (nearly everything except alphanumeric).
|
|
// This is done in ResponseHeaders::SetPageSpeedQueryParamsAsCookies().
|
|
// [So now we're here, where we use the cookie values set by the above steps]
|
|
// * We unescape the cookie value to reverse the previous step.
|
|
// Note that GoogleUrl::UnescapeQueryParam(GoogleUrl::EscapeQueryParam(x))
|
|
// is the identify function, so we expect the value to be GoogleUrl
|
|
// minimally escaped.
|
|
// TODO(sligocki): GoogleUrl::UnescapeIgnorePlus(GoogleUrl::EscapeQueryParam)
|
|
// is not the identity function. Is this a problem?
|
|
// * We sanitize the unescaped cookie value by dummying up a GoogleUrl with
|
|
// the value as a query parameter value, hence re-minimally escaping it.
|
|
// * We then escape this sanitized value since that's the process that we
|
|
// went through above: if this escaped value equals the cookie value then
|
|
// the cookie value seems authentic -and- the unescaped value must also be
|
|
// sanitized, meaning it's safe to pass to our value parsing logic. If the
|
|
// escaped value does -not- equal the cookie value, it means the cookie has
|
|
// characters that we cannot have put there, and we assume that someone has
|
|
// manually set it in an attempt to circumvent our precautions, so we
|
|
// ignore the cookie completely.
|
|
RequestHeaders::CookieMultimapConstIter it, end;
|
|
for (it = all_cookies.begin(), end = all_cookies.end(); it != end; ++it) {
|
|
StringPiece cookie_name = it->first;
|
|
if (MightBeCustomOption(cookie_name)) {
|
|
GoogleUrl gurl;
|
|
StringPiece cookie_value = it->second.first;
|
|
GoogleString unescaped = GoogleUrl::UnescapeIgnorePlus(cookie_value);
|
|
StringPiece sanitized = SanitizeValueAsQP(unescaped, &gurl);
|
|
GoogleString escaped = GoogleUrl::EscapeQueryParam(sanitized);
|
|
if (unescaped == sanitized && escaped == cookie_value) {
|
|
RequestContextPtr null_request_context;
|
|
if (ScanNameValue(cookie_name, unescaped, allow_options,
|
|
null_request_context, request_properties.get(),
|
|
options_.get(), handler) == kSuccess) {
|
|
pagespeed_option_cookies_.AddEscaped(cookie_name, unescaped);
|
|
status = kSuccess;
|
|
}
|
|
} else {
|
|
// PageSpeed cookies with an invalid value will not be cleared. This
|
|
// is unfortunate but OK since they'll never apply (due to the invalid
|
|
// value) and will eventually expire anyway.
|
|
handler->Message(kInfo, "PageSpeed Cookie value seems mangled: "
|
|
"name='%s', value='%s', escaped value='%s'",
|
|
cookie_name.as_string().c_str(),
|
|
cookie_value.as_string().c_str(),
|
|
escaped.c_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
pagespeed_query_params_.Clear();
|
|
QueryParams temp_query_params;
|
|
for (int i = 0; i < query_params_.size(); ++i) {
|
|
GoogleString unescaped_value;
|
|
if (query_params_.UnescapedValue(i, &unescaped_value)) {
|
|
// The Unescaper changes "+" to " ", which is not what we want, and
|
|
// is not what happens for response headers and request headers, so
|
|
// let's fix it now.
|
|
GlobalReplaceSubstring(" " , "+", &unescaped_value);
|
|
switch (ScanNameValue(
|
|
query_params_.name(i), unescaped_value, allow_options,
|
|
request_context, request_properties.get(), options_.get(), handler)) {
|
|
case kNoneFound:
|
|
// If this is not a PageSpeed-related query-parameter, then save it
|
|
// in its escaped form.
|
|
temp_query_params.AddEscaped(query_params_.name(i),
|
|
*query_params_.EscapedValue(i));
|
|
break;
|
|
case kSuccess:
|
|
// If it is a PageSpeed-related query parameter, also save it so we
|
|
// can add it back if we receive a redirection response to our fetch.
|
|
pagespeed_query_params_.AddEscaped(query_params_.name(i),
|
|
*query_params_.EscapedValue(i));
|
|
status = kSuccess;
|
|
break;
|
|
case kInvalid:
|
|
status = kInvalid;
|
|
options_.reset(NULL);
|
|
return status;
|
|
}
|
|
} else {
|
|
temp_query_params.AddEscaped(query_params_.name(i), StringPiece());
|
|
}
|
|
}
|
|
if (status == kSuccess) {
|
|
// Remove the ModPagespeed* or PageSpeed* for url.
|
|
GoogleString temp_params = temp_query_params.empty() ? "" :
|
|
StrCat("?", temp_query_params.ToEscapedString());
|
|
request_url->Reset(StrCat(request_url->AllExceptQuery(), temp_params,
|
|
request_url->AllAfterQuery()));
|
|
}
|
|
|
|
switch (ScanHeader<RequestHeaders>(
|
|
allow_options, request_option_override, request_context, request_headers,
|
|
request_properties.get(), options_.get(), handler)) {
|
|
case kNoneFound:
|
|
break;
|
|
case kSuccess:
|
|
status = kSuccess;
|
|
break;
|
|
case kInvalid:
|
|
status = kInvalid;
|
|
options_.reset(NULL);
|
|
return status;
|
|
}
|
|
|
|
switch (ScanHeader<ResponseHeaders>(
|
|
allow_options, request_option_override, request_context, response_headers,
|
|
request_properties.get(), options_.get(), handler)) {
|
|
case kNoneFound:
|
|
break;
|
|
case kSuccess:
|
|
status = kSuccess;
|
|
break;
|
|
case kInvalid:
|
|
status = kInvalid;
|
|
options_.reset(NULL);
|
|
return status;
|
|
}
|
|
|
|
// Set a default rewrite level in case the mod_pagespeed server has no
|
|
// rewriting options configured.
|
|
// Note that if any filters are explicitly set with
|
|
// PageSpeedFilters=..., then the call to
|
|
// DisableAllFiltersNotExplicitlyEnabled() below will make the 'level'
|
|
// irrelevant.
|
|
switch (status) {
|
|
case kSuccess:
|
|
options_->SetDefaultRewriteLevel(RewriteOptions::kCoreFilters);
|
|
break;
|
|
case kNoneFound:
|
|
options_.reset(NULL);
|
|
break;
|
|
case kInvalid:
|
|
LOG(DFATAL) << "Invalid responses always use early exit";
|
|
options_.reset(NULL);
|
|
break;
|
|
}
|
|
return status;
|
|
}
|
|
|
|
bool RewriteQuery::MightBeCustomOption(StringPiece name) {
|
|
// TODO(jmarantz): switch to case-insenstive comparisons for these prefixes.
|
|
return name.starts_with(kModPagespeed) || name.starts_with(kPageSpeed) ||
|
|
StringCaseEqual(name, HttpAttributes::kXPsaClientOptions);
|
|
}
|
|
|
|
template <class HeaderT>
|
|
bool RewriteQuery::HeadersMayHaveCustomOptions(const QueryParams& params,
|
|
const HeaderT* headers) {
|
|
if (headers != NULL) {
|
|
for (int i = 0, n = headers->NumAttributes(); i < n; ++i) {
|
|
if (MightBeCustomOption(headers->Name(i))) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool RewriteQuery::CookiesMayHaveCustomOptions(
|
|
const RequestHeaders::CookieMultimap& cookies) {
|
|
RequestHeaders::CookieMultimapConstIter it = cookies.begin();
|
|
RequestHeaders::CookieMultimapConstIter end = cookies.end();
|
|
for (; it != end; ++it) {
|
|
if (MightBeCustomOption(it->first)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool RewriteQuery::MayHaveCustomOptions(
|
|
const QueryParams& params, const RequestHeaders* req_headers,
|
|
const ResponseHeaders* resp_headers,
|
|
const RequestHeaders::CookieMultimap& cookies) {
|
|
for (int i = 0, n = params.size(); i < n; ++i) {
|
|
if (MightBeCustomOption(params.name(i))) {
|
|
return true;
|
|
}
|
|
}
|
|
if (HeadersMayHaveCustomOptions(params, req_headers)) {
|
|
return true;
|
|
}
|
|
if (HeadersMayHaveCustomOptions(params, resp_headers)) {
|
|
return true;
|
|
}
|
|
if (CookiesMayHaveCustomOptions(cookies)) {
|
|
return true;
|
|
}
|
|
if (req_headers != NULL &&
|
|
(req_headers->Has(HttpAttributes::kXPsaClientOptions) ||
|
|
req_headers->HasValue(HttpAttributes::kCacheControl, "no-transform"))) {
|
|
return true;
|
|
}
|
|
if ((resp_headers != NULL) && resp_headers->HasValue(
|
|
HttpAttributes::kCacheControl, "no-transform")) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
RewriteQuery::Status RewriteQuery::ScanNameValue(
|
|
const StringPiece& name, const StringPiece& value, bool allow_options,
|
|
const RequestContextPtr& request_context,
|
|
RequestProperties* request_properties, RewriteOptions* options,
|
|
MessageHandler* handler) {
|
|
Status status = kNoneFound;
|
|
|
|
// See https://github.com/pagespeed/mod_pagespeed/issues/627
|
|
// Evidently bots and other clients may not properly resolve the quoted
|
|
// URLs we send into noscript links, so remove any excess quoting we
|
|
// see around the value.
|
|
StringPiece trimmed_value(value);
|
|
TrimUrlQuotes(&trimmed_value);
|
|
if (name == kModPagespeed || name == kPageSpeed) {
|
|
RewriteOptions::EnabledEnum enabled;
|
|
if (RewriteOptions::ParseFromString(trimmed_value, &enabled)) {
|
|
options->set_enabled(enabled);
|
|
status = kSuccess;
|
|
} else if (trimmed_value.starts_with(kNoscriptValue)) {
|
|
// We use starts_with("noscript") to help resolve Issue 874.
|
|
// Disable filters that depend on custom script execution.
|
|
options->DisableFiltersRequiringScriptExecution();
|
|
options->EnableFilter(RewriteOptions::kHandleNoscriptRedirect);
|
|
status = kSuccess;
|
|
} else {
|
|
// TODO(sligocki): Return 404s instead of logging server errors here
|
|
// and below.
|
|
handler->Message(kWarning, "Invalid value for %s: %s "
|
|
"(should be on, off, unplugged, or noscript)",
|
|
name.as_string().c_str(),
|
|
trimmed_value.as_string().c_str());
|
|
status = kInvalid;
|
|
}
|
|
} else if (!allow_options) {
|
|
status = kNoneFound;
|
|
} else if (name == kModPagespeedFilters || name == kPageSpeedFilters) {
|
|
// When using PageSpeedFilters query param, only the specified filters
|
|
// should be enabled.
|
|
if (options->AdjustFiltersByCommaSeparatedList(trimmed_value, handler)) {
|
|
status = kSuccess;
|
|
} else {
|
|
status = kInvalid;
|
|
}
|
|
} else if (StringCaseEqual(name, HttpAttributes::kXPsaClientOptions)) {
|
|
if (UpdateRewriteOptionsWithClientOptions(
|
|
trimmed_value, request_properties, options)) {
|
|
status = kSuccess;
|
|
}
|
|
// We don't want to return kInvalid, which causes 405 (kMethodNotAllowed)
|
|
// returned to client.
|
|
} else if (StringCaseEqual(name, HttpAttributes::kCacheControl)) {
|
|
StringPieceVector pairs;
|
|
SplitStringPieceToVector(trimmed_value, ",", &pairs,
|
|
true /* omit_empty_strings */);
|
|
for (int i = 0, n = pairs.size(); i < n; ++i) {
|
|
TrimWhitespace(&pairs[i]);
|
|
if (pairs[i] == "no-transform") {
|
|
// TODO(jmarantz): A .pagespeed resource should return un-optimized
|
|
// content with "Cache-Control: no-transform".
|
|
options->set_enabled(RewriteOptions::kEnabledStandby);
|
|
status = kSuccess;
|
|
break;
|
|
}
|
|
}
|
|
} else if (name.starts_with(kModPagespeed) || name.starts_with(kPageSpeed)) {
|
|
// Remove the initial ModPagespeed or PageSpeed.
|
|
StringPiece name_suffix = name;
|
|
stringpiece_ssize_type prefix_len;
|
|
if (name.starts_with(kModPagespeed)) {
|
|
prefix_len = sizeof(kModPagespeed)-1;
|
|
} else {
|
|
prefix_len = sizeof(kPageSpeed)-1;
|
|
}
|
|
name_suffix.remove_prefix(prefix_len);
|
|
switch (options->SetOptionFromQuery(name_suffix, trimmed_value)) {
|
|
case RewriteOptions::kOptionOk:
|
|
status = kSuccess;
|
|
break;
|
|
case RewriteOptions::kOptionNameUnknown:
|
|
if (request_context.get() != NULL &&
|
|
StringCaseEqual(name_suffix,
|
|
RewriteOptions::kStickyQueryParameters)) {
|
|
request_context->set_sticky_query_parameters_token(trimmed_value);
|
|
status = kSuccess;
|
|
} else {
|
|
status = kNoneFound;
|
|
}
|
|
break;
|
|
case RewriteOptions::kOptionValueInvalid:
|
|
status = kInvalid;
|
|
break;
|
|
}
|
|
}
|
|
return status;
|
|
}
|
|
|
|
// In some environments it is desirable to bind a URL to the options
|
|
// that affect it. One example of where this would be needed is if
|
|
// images are served by a separate cluster that doesn't share the same
|
|
// configuration as the mod_pagespeed instances that rewrote the HTML.
|
|
// In this case, we must encode the relevant options as query-params
|
|
// to be appended to the URL. These should be decodable by Scan()
|
|
// above, though they don't need to be in the same verbose format that
|
|
// we document for debugging and experimentation. They can use the
|
|
// more concise abbreviations of 2-4 letters for each option.
|
|
GoogleString RewriteQuery::GenerateResourceOption(
|
|
StringPiece filter_id, RewriteDriver* driver) {
|
|
const RewriteFilter* filter = driver->FindFilter(filter_id);
|
|
// TODO(sligocki): We do not seem to be detecting Apache crashes in the
|
|
// system_test. We should detect and fail when these crashes occur.
|
|
CHECK(filter != NULL)
|
|
<< "Filter ID " << filter_id << " is not registered in RewriteDriver. "
|
|
<< "You must register it with a call to RegisterRewriteFilter() in "
|
|
<< "RewriteDriver::SetServerContext().";
|
|
StringPiece prefix("");
|
|
GoogleString value;
|
|
const RewriteOptions* options = driver->options();
|
|
|
|
// All the filters & options will be encoded into the value of a
|
|
// single query param with name kAddQueryFromOptionName ("PsolOpt").
|
|
// The value will have the comma-separated filters IDs, and option IDs,
|
|
// which are all given a 2-4 letter codes. The only difference between
|
|
// options & filters syntactically is that options have values preceded
|
|
// by a colon:
|
|
// filter1,filter2,filter3,option1:value1,option2:value2
|
|
|
|
// Add any relevant enabled filters.
|
|
int num_filters;
|
|
const RewriteOptions::Filter* filters = filter->RelatedFilters(&num_filters);
|
|
for (int i = 0; i < num_filters; ++i) {
|
|
RewriteOptions::Filter filter_enum = filters[i];
|
|
if (options->Enabled(filter_enum)) {
|
|
StrAppend(&value, prefix, RewriteOptions::FilterId(filter_enum));
|
|
prefix = kResourceFilterSeparator;
|
|
}
|
|
}
|
|
|
|
// Add any non-default options.
|
|
GoogleString option_value;
|
|
const StringPieceVector* opts = filter->RelatedOptions();
|
|
for (int i = 0, n = (opts == NULL ? 0 : opts->size()); i < n; ++i) {
|
|
StringPiece option = (*opts)[i];
|
|
const char* id;
|
|
bool was_set = false;
|
|
if (options->OptionValue(option, &id, &was_set, &option_value) && was_set) {
|
|
StrAppend(&value, prefix, id, kResourceOptionValueSeparator,
|
|
option_value);
|
|
prefix = kResourceFilterSeparator;
|
|
}
|
|
}
|
|
return value;
|
|
}
|
|
|
|
RewriteQuery::Status RewriteQuery::ParseResourceOption(
|
|
StringPiece value, RewriteOptions* options, const RewriteFilter* filter) {
|
|
Status status = kNoneFound;
|
|
StringPieceVector filters_and_options;
|
|
SplitStringPieceToVector(value, kResourceFilterSeparator,
|
|
&filters_and_options, true);
|
|
|
|
// We will want to validate any filters & options we are trying to set
|
|
// with this mechanism against the whitelist of whatever the filter thinks is
|
|
// needed. But do this lazily.
|
|
int num_filters;
|
|
const RewriteOptions::Filter* filters = filter->RelatedFilters(&num_filters);
|
|
const StringPieceVector* opts = filter->RelatedOptions();
|
|
|
|
for (int i = 0, n = filters_and_options.size(); i < n; ++i) {
|
|
StringPieceVector name_value;
|
|
SplitStringPieceToVector(filters_and_options[i],
|
|
kResourceOptionValueSeparator, &name_value, true);
|
|
switch (name_value.size()) {
|
|
case 1: {
|
|
RewriteOptions::Filter filter_enum =
|
|
RewriteOptions::LookupFilterById(name_value[0]);
|
|
if ((filter_enum == RewriteOptions::kEndOfFilters) ||
|
|
!std::binary_search(filters, filters + num_filters, filter_enum)) {
|
|
status = kInvalid;
|
|
} else {
|
|
options->EnableFilter(filter_enum);
|
|
status = kSuccess;
|
|
}
|
|
break;
|
|
}
|
|
case 2: {
|
|
StringPiece option_name =
|
|
RewriteOptions::LookupOptionNameById(name_value[0]);
|
|
if (!option_name.empty() &&
|
|
opts != NULL &&
|
|
std::binary_search(opts->begin(), opts->end(), option_name) &&
|
|
options->SetOptionFromName(option_name, name_value[1])
|
|
== RewriteOptions::kOptionOk) {
|
|
status = kSuccess;
|
|
} else {
|
|
status = kInvalid;
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
status = kInvalid;
|
|
}
|
|
}
|
|
options->SetRewriteLevel(RewriteOptions::kPassThrough);
|
|
options->DisableAllFiltersNotExplicitlyEnabled();
|
|
return status;
|
|
}
|
|
|
|
bool RewriteQuery::ParseProxyMode(
|
|
const GoogleString* mode_name, ProxyMode* mode) {
|
|
int mode_value = 0;
|
|
if (mode_name != NULL &&
|
|
!mode_name->empty() &&
|
|
StringToInt(*mode_name, &mode_value) &&
|
|
mode_value >= kProxyModeDefault &&
|
|
mode_value <= kProxyModeNoTransform) {
|
|
*mode = static_cast<ProxyMode>(mode_value);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool RewriteQuery::ParseImageQualityPreference(
|
|
const GoogleString* preference_value,
|
|
DeviceProperties::ImageQualityPreference* preference) {
|
|
int value = 0;
|
|
if (preference_value != NULL &&
|
|
!preference_value->empty() &&
|
|
StringToInt(*preference_value, &value) &&
|
|
value >= DeviceProperties::kImageQualityDefault &&
|
|
value <= DeviceProperties::kImageQualityHigh) {
|
|
*preference = static_cast<DeviceProperties::ImageQualityPreference>(value);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool RewriteQuery::ParseClientOptions(
|
|
const StringPiece& client_options, ProxyMode* proxy_mode,
|
|
DeviceProperties::ImageQualityPreference* image_quality_preference) {
|
|
StringMultiMapSensitive options;
|
|
options.AddFromNameValuePairs(
|
|
client_options, kProxyOptionSeparator, kProxyOptionValueSeparator,
|
|
true);
|
|
|
|
const GoogleString* version_value = options.Lookup1(kProxyOptionVersion);
|
|
// We only support version value of kProxyOptionValidVersionValue for now.
|
|
// New supported version might be added later.
|
|
if (version_value != NULL &&
|
|
*version_value == kProxyOptionValidVersionValue) {
|
|
*proxy_mode = kProxyModeDefault;
|
|
*image_quality_preference = DeviceProperties::kImageQualityDefault;
|
|
ParseProxyMode(options.Lookup1(kProxyOptionMode), proxy_mode);
|
|
|
|
if (*proxy_mode == kProxyModeDefault) {
|
|
ParseImageQualityPreference(
|
|
options.Lookup1(kProxyOptionImageQualityPreference),
|
|
image_quality_preference);
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool RewriteQuery::UpdateRewriteOptionsWithClientOptions(
|
|
StringPiece client_options, RequestProperties* request_properties,
|
|
RewriteOptions* options) {
|
|
ProxyMode proxy_mode = kProxyModeDefault;
|
|
DeviceProperties::ImageQualityPreference quality_preference =
|
|
DeviceProperties::kImageQualityDefault;
|
|
if (!ParseClientOptions(client_options, &proxy_mode, &quality_preference)) {
|
|
return false;
|
|
}
|
|
|
|
if (proxy_mode == kProxyModeNoTransform) {
|
|
options->DisableAllFilters();
|
|
return true;
|
|
} else if (proxy_mode == kProxyModeNoImageTransform) {
|
|
ImageRewriteFilter::DisableRelatedFilters(options);
|
|
return true;
|
|
} else if (proxy_mode == kProxyModeDefault) {
|
|
return false;
|
|
}
|
|
DCHECK(false);
|
|
return false;
|
|
}
|
|
|
|
} // namespace net_instaweb
|