| // 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 insert js. Otherwise, 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. |
| // |
| // 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. |
| if (request_headers != NULL && request_headers->IsXmlHttpRequest()) { |
| if (options_.get() == NULL) { |
| options_.reset(factory->NewRewriteOptionsForQuery()); |
| } |
| options_->DisableFiltersRequiringScriptExecution(); |
| options_->DisableFilter(RewriteOptions::kPrioritizeCriticalCss); |
| } |
| |
| // 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), NULL); |
| } |
| } |
| 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://code.google.com/p/modpagespeed/issues/detail?id=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::kEnabledOff); |
| 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 |