|  | /* | 
|  | * 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 "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/apache/incubator-pagespeed-mod/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 |