| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "extensions/browser/api/web_request/web_request_api_helpers.h" |
| |
| #include <stddef.h> |
| #include <stdint.h> |
| |
| #include <cmath> |
| #include <string_view> |
| #include <tuple> |
| #include <utility> |
| |
| #include "base/containers/adapters.h" |
| #include "base/containers/contains.h" |
| #include "base/containers/fixed_flat_map.h" |
| #include "base/containers/fixed_flat_set.h" |
| #include "base/functional/bind.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/ranges/algorithm.h" |
| #include "base/stl_util.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "base/time/time.h" |
| #include "base/values.h" |
| #include "build/chromeos_buildflags.h" |
| #include "components/web_cache/browser/web_cache_manager.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/render_process_host.h" |
| #include "extensions/browser/api/declarative_net_request/constants.h" |
| #include "extensions/browser/api/declarative_net_request/request_action.h" |
| #include "extensions/browser/api/extensions_api_client.h" |
| #include "extensions/browser/api/web_request/web_request_api_constants.h" |
| #include "extensions/browser/api/web_request/web_request_info.h" |
| #include "extensions/browser/extension_system.h" |
| #include "extensions/browser/extensions_browser_client.h" |
| #include "extensions/common/api/declarative_net_request.h" |
| #include "extensions/common/extension_id.h" |
| #include "net/cookies/cookie_util.h" |
| #include "net/cookies/parsed_cookie.h" |
| #include "net/http/http_request_headers.h" |
| #include "net/http/http_util.h" |
| #include "net/log/net_log_event_type.h" |
| #include "services/network/public/cpp/features.h" |
| #include "url/url_constants.h" |
| |
| // TODO(battre): move all static functions into an anonymous namespace at the |
| // top of this file. |
| |
| using base::Time; |
| using net::cookie_util::ParsedRequestCookie; |
| using net::cookie_util::ParsedRequestCookies; |
| |
| namespace keys = extension_web_request_api_constants; |
| namespace web_request = extensions::api::web_request; |
| using DNRRequestAction = extensions::declarative_net_request::RequestAction; |
| |
| namespace extension_web_request_api_helpers { |
| |
| namespace { |
| |
| namespace dnr_api = extensions::api::declarative_net_request; |
| using ParsedResponseCookies = std::vector<std::unique_ptr<net::ParsedCookie>>; |
| |
| void ClearCacheOnNavigationOnUI() { |
| extensions::ExtensionsBrowserClient::Get()->ClearBackForwardCache(); |
| web_cache::WebCacheManager::GetInstance()->ClearCacheOnNavigation(); |
| } |
| |
| bool ParseCookieLifetime(const net::ParsedCookie& cookie, |
| int64_t* seconds_till_expiry) { |
| // 'Max-Age' is processed first because according to: |
| // http://tools.ietf.org/html/rfc6265#section-5.3 'Max-Age' attribute |
| // overrides 'Expires' attribute. |
| if (cookie.HasMaxAge() && |
| base::StringToInt64(cookie.MaxAge(), seconds_till_expiry)) { |
| return true; |
| } |
| |
| Time parsed_expiry_time; |
| if (cookie.HasExpires()) { |
| parsed_expiry_time = |
| net::cookie_util::ParseCookieExpirationTime(cookie.Expires()); |
| } |
| |
| if (!parsed_expiry_time.is_null()) { |
| *seconds_till_expiry = |
| ceil((parsed_expiry_time - Time::Now()).InSecondsF()); |
| return *seconds_till_expiry >= 0; |
| } |
| return false; |
| } |
| |
| void RecordRequestHeaderRemoved(RequestHeaderType type) { |
| UMA_HISTOGRAM_ENUMERATION("Extensions.WebRequest.RequestHeaderRemoved", type); |
| } |
| |
| void RecordRequestHeaderAdded(RequestHeaderType type) { |
| UMA_HISTOGRAM_ENUMERATION("Extensions.WebRequest.RequestHeaderAdded", type); |
| } |
| |
| void RecordRequestHeaderChanged(RequestHeaderType type) { |
| UMA_HISTOGRAM_ENUMERATION("Extensions.WebRequest.RequestHeaderChanged", type); |
| } |
| |
| void RecordDNRRequestHeaderRemoved(RequestHeaderType type) { |
| UMA_HISTOGRAM_ENUMERATION( |
| "Extensions.DeclarativeNetRequest.RequestHeaderRemoved", type); |
| } |
| |
| void RecordDNRRequestHeaderAdded(RequestHeaderType type) { |
| UMA_HISTOGRAM_ENUMERATION( |
| "Extensions.DeclarativeNetRequest.RequestHeaderAdded", type); |
| } |
| |
| void RecordDNRRequestHeaderChanged(RequestHeaderType type) { |
| UMA_HISTOGRAM_ENUMERATION( |
| "Extensions.DeclarativeNetRequest.RequestHeaderChanged", type); |
| } |
| |
| bool IsStringLowerCaseASCII(std::string_view s) { |
| return base::ranges::none_of(s, base::IsAsciiUpper<char>); |
| } |
| |
| constexpr auto kRequestHeaderEntries = |
| base::MakeFixedFlatMap<std::string_view, RequestHeaderType>( |
| {{"accept", RequestHeaderType::kAccept}, |
| {"accept-charset", RequestHeaderType::kAcceptCharset}, |
| {"accept-encoding", RequestHeaderType::kAcceptEncoding}, |
| {"accept-language", RequestHeaderType::kAcceptLanguage}, |
| {"access-control-request-headers", |
| RequestHeaderType::kAccessControlRequestHeaders}, |
| {"access-control-request-method", |
| RequestHeaderType::kAccessControlRequestMethod}, |
| {"authorization", RequestHeaderType::kAuthorization}, |
| {"cache-control", RequestHeaderType::kCacheControl}, |
| {"connection", RequestHeaderType::kConnection}, |
| {"content-encoding", RequestHeaderType::kContentEncoding}, |
| {"content-language", RequestHeaderType::kContentLanguage}, |
| {"content-length", RequestHeaderType::kContentLength}, |
| {"content-location", RequestHeaderType::kContentLocation}, |
| {"content-type", RequestHeaderType::kContentType}, |
| {"cookie", RequestHeaderType::kCookie}, |
| {"date", RequestHeaderType::kDate}, |
| {"dnt", RequestHeaderType::kDnt}, |
| {"early-data", RequestHeaderType::kEarlyData}, |
| {"expect", RequestHeaderType::kExpect}, |
| {"forwarded", RequestHeaderType::kForwarded}, |
| {"from", RequestHeaderType::kFrom}, |
| {"host", RequestHeaderType::kHost}, |
| {"if-match", RequestHeaderType::kIfMatch}, |
| {"if-modified-since", RequestHeaderType::kIfModifiedSince}, |
| {"if-none-match", RequestHeaderType::kIfNoneMatch}, |
| {"if-range", RequestHeaderType::kIfRange}, |
| {"if-unmodified-since", RequestHeaderType::kIfUnmodifiedSince}, |
| {"keep-alive", RequestHeaderType::kKeepAlive}, |
| {"origin", RequestHeaderType::kOrigin}, |
| {"pragma", RequestHeaderType::kPragma}, |
| {"proxy-authorization", RequestHeaderType::kProxyAuthorization}, |
| {"proxy-connection", RequestHeaderType::kProxyConnection}, |
| {"range", RequestHeaderType::kRange}, |
| {"referer", RequestHeaderType::kReferer}, |
| {"te", RequestHeaderType::kTe}, |
| {"transfer-encoding", RequestHeaderType::kTransferEncoding}, |
| {"upgrade", RequestHeaderType::kUpgrade}, |
| {"upgrade-insecure-requests", |
| RequestHeaderType::kUpgradeInsecureRequests}, |
| {"user-agent", RequestHeaderType::kUserAgent}, |
| {"via", RequestHeaderType::kVia}, |
| {"warning", RequestHeaderType::kWarning}, |
| {"x-forwarded-for", RequestHeaderType::kXForwardedFor}, |
| {"x-forwarded-host", RequestHeaderType::kXForwardedHost}, |
| {"x-forwarded-proto", RequestHeaderType::kXForwardedProto}}); |
| |
| constexpr bool IsValidHeaderName(std::string_view str) { |
| for (char ch : str) { |
| if ((ch < 'a' || ch > 'z') && ch != '-') |
| return false; |
| } |
| return true; |
| } |
| |
| template <typename T> |
| constexpr bool ValidateHeaderEntries(const T& entries) { |
| for (const auto& entry : entries) { |
| if (!IsValidHeaderName(entry.first)) |
| return false; |
| } |
| return true; |
| } |
| |
| // All entries other than kOther and kNone are mapped. |
| // sec-origin-policy was removed. |
| // So -2 is -1 for the count of the enums, and -1 for the removed |
| // sec-origin-policy which does not have a corresponding entry in |
| // kRequestHeaderEntries but does contribute to RequestHeaderType::kMaxValue. |
| static_assert(static_cast<size_t>(RequestHeaderType::kMaxValue) - 2 == |
| kRequestHeaderEntries.size(), |
| "Invalid number of request header entries"); |
| |
| static_assert(ValidateHeaderEntries(kRequestHeaderEntries), |
| "Invalid request header entries"); |
| |
| // Uses |record_func| to record |header|. If |header| is not recorded, false is |
| // returned. |
| void RecordRequestHeader(const std::string& header, |
| void (*record_func)(RequestHeaderType)) { |
| DCHECK(IsStringLowerCaseASCII(header)); |
| const auto it = kRequestHeaderEntries.find(header); |
| record_func(it != kRequestHeaderEntries.end() ? it->second |
| : RequestHeaderType::kOther); |
| } |
| |
| void RecordResponseHeaderChanged(ResponseHeaderType type) { |
| UMA_HISTOGRAM_ENUMERATION("Extensions.WebRequest.ResponseHeaderChanged", |
| type); |
| } |
| |
| void RecordResponseHeaderAdded(ResponseHeaderType type) { |
| UMA_HISTOGRAM_ENUMERATION("Extensions.WebRequest.ResponseHeaderAdded", type); |
| } |
| |
| void RecordResponseHeaderRemoved(ResponseHeaderType type) { |
| UMA_HISTOGRAM_ENUMERATION("Extensions.WebRequest.ResponseHeaderRemoved", |
| type); |
| } |
| |
| void RecordDNRResponseHeaderChanged(ResponseHeaderType type) { |
| UMA_HISTOGRAM_ENUMERATION( |
| "Extensions.DeclarativeNetRequest.ResponseHeaderChanged", type); |
| } |
| |
| void RecordDNRResponseHeaderAdded(ResponseHeaderType type) { |
| UMA_HISTOGRAM_ENUMERATION( |
| "Extensions.DeclarativeNetRequest.ResponseHeaderAdded", type); |
| } |
| |
| void RecordDNRResponseHeaderRemoved(ResponseHeaderType type) { |
| UMA_HISTOGRAM_ENUMERATION( |
| "Extensions.DeclarativeNetRequest.ResponseHeaderRemoved", type); |
| } |
| |
| constexpr auto kResponseHeaderEntries = |
| base::MakeFixedFlatMap<std::string_view, ResponseHeaderType>({ |
| {"accept-patch", ResponseHeaderType::kAcceptPatch}, |
| {"accept-ranges", ResponseHeaderType::kAcceptRanges}, |
| {"access-control-allow-credentials", |
| ResponseHeaderType::kAccessControlAllowCredentials}, |
| {"access-control-allow-headers", |
| ResponseHeaderType::kAccessControlAllowHeaders}, |
| {"access-control-allow-methods", |
| ResponseHeaderType::kAccessControlAllowMethods}, |
| {"access-control-allow-origin", |
| ResponseHeaderType::kAccessControlAllowOrigin}, |
| {"access-control-expose-headers", |
| ResponseHeaderType::kAccessControlExposeHeaders}, |
| {"access-control-max-age", ResponseHeaderType::kAccessControlMaxAge}, |
| {"age", ResponseHeaderType::kAge}, |
| {"allow", ResponseHeaderType::kAllow}, |
| {"alt-svc", ResponseHeaderType::kAltSvc}, |
| {"cache-control", ResponseHeaderType::kCacheControl}, |
| {"clear-site-data", ResponseHeaderType::kClearSiteData}, |
| {"connection", ResponseHeaderType::kConnection}, |
| {"content-disposition", ResponseHeaderType::kContentDisposition}, |
| {"content-encoding", ResponseHeaderType::kContentEncoding}, |
| {"content-language", ResponseHeaderType::kContentLanguage}, |
| {"content-length", ResponseHeaderType::kContentLength}, |
| {"content-location", ResponseHeaderType::kContentLocation}, |
| {"content-range", ResponseHeaderType::kContentRange}, |
| {"content-security-policy", ResponseHeaderType::kContentSecurityPolicy}, |
| {"content-security-policy-report-only", |
| ResponseHeaderType::kContentSecurityPolicyReportOnly}, |
| {"content-type", ResponseHeaderType::kContentType}, |
| {"date", ResponseHeaderType::kDate}, |
| {"etag", ResponseHeaderType::kETag}, |
| {"expect-ct", ResponseHeaderType::kExpectCT}, |
| {"expires", ResponseHeaderType::kExpires}, |
| {"feature-policy", ResponseHeaderType::kFeaturePolicy}, |
| {"keep-alive", ResponseHeaderType::kKeepAlive}, |
| {"large-allocation", ResponseHeaderType::kLargeAllocation}, |
| {"last-modified", ResponseHeaderType::kLastModified}, |
| {"location", ResponseHeaderType::kLocation}, |
| {"pragma", ResponseHeaderType::kPragma}, |
| {"proxy-authenticate", ResponseHeaderType::kProxyAuthenticate}, |
| {"proxy-connection", ResponseHeaderType::kProxyConnection}, |
| {"public-key-pins", ResponseHeaderType::kPublicKeyPins}, |
| {"public-key-pins-report-only", |
| ResponseHeaderType::kPublicKeyPinsReportOnly}, |
| {"referrer-policy", ResponseHeaderType::kReferrerPolicy}, |
| {"refresh", ResponseHeaderType::kRefresh}, |
| {"retry-after", ResponseHeaderType::kRetryAfter}, |
| {"sec-websocket-accept", ResponseHeaderType::kSecWebSocketAccept}, |
| {"server", ResponseHeaderType::kServer}, |
| {"server-timing", ResponseHeaderType::kServerTiming}, |
| {"set-cookie", ResponseHeaderType::kSetCookie}, |
| {"sourcemap", ResponseHeaderType::kSourceMap}, |
| {"strict-transport-security", |
| ResponseHeaderType::kStrictTransportSecurity}, |
| {"timing-allow-origin", ResponseHeaderType::kTimingAllowOrigin}, |
| {"tk", ResponseHeaderType::kTk}, |
| {"trailer", ResponseHeaderType::kTrailer}, |
| {"transfer-encoding", ResponseHeaderType::kTransferEncoding}, |
| {"upgrade", ResponseHeaderType::kUpgrade}, |
| {"vary", ResponseHeaderType::kVary}, |
| {"via", ResponseHeaderType::kVia}, |
| {"warning", ResponseHeaderType::kWarning}, |
| {"www-authenticate", ResponseHeaderType::kWWWAuthenticate}, |
| {"x-content-type-options", ResponseHeaderType::kXContentTypeOptions}, |
| {"x-dns-prefetch-control", ResponseHeaderType::kXDNSPrefetchControl}, |
| {"x-frame-options", ResponseHeaderType::kXFrameOptions}, |
| {"x-xss-protection", ResponseHeaderType::kXXSSProtection}, |
| }); |
| |
| void RecordResponseHeader(std::string_view header, |
| void (*record_func)(ResponseHeaderType)) { |
| DCHECK(IsStringLowerCaseASCII(header)); |
| const auto it = kResponseHeaderEntries.find(header); |
| record_func(it != kResponseHeaderEntries.end() ? it->second |
| : ResponseHeaderType::kOther); |
| } |
| |
| // All entries other than kOther and kNone are mapped. |
| static_assert(static_cast<size_t>(ResponseHeaderType::kMaxValue) - 1 == |
| kResponseHeaderEntries.size(), |
| "Invalid number of response header entries"); |
| |
| static_assert(ValidateHeaderEntries(kResponseHeaderEntries), |
| "Invalid response header entries"); |
| |
| // Returns the new value for the header with `header_name` after `operation` is |
| // applied to it with the specified `header_value`. This will just return |
| // `header_value` unless the operation is APPEND and the header already exists, |
| // which will return <existing header value><delimiter><`header_value`>. |
| std::string GetDNRNewRequestHeaderValue(net::HttpRequestHeaders* headers, |
| const std::string& header_name, |
| const std::string& header_value, |
| dnr_api::HeaderOperation operation) { |
| namespace dnr = extensions::declarative_net_request; |
| |
| std::string existing_value; |
| bool has_header = headers->GetHeader(header_name, &existing_value); |
| |
| if (has_header && operation == dnr_api::HeaderOperation::kAppend) { |
| const auto it = dnr::kDNRRequestHeaderAppendAllowList.find(header_name); |
| DCHECK(it != dnr::kDNRRequestHeaderAppendAllowList.end()); |
| return base::StrCat({existing_value, it->second, header_value}); |
| } |
| |
| return header_value; |
| } |
| |
| // Represents an action to be taken on a given header. |
| struct DNRHeaderAction { |
| DNRHeaderAction(const DNRRequestAction::HeaderInfo* header_info, |
| const extensions::ExtensionId* extension_id) |
| : header_info(header_info), extension_id(extension_id) {} |
| |
| // Returns whether for the same header, the operation specified by |
| // |next_action| conflicts with the operation specified by this action. |
| bool ConflictsWithSubsequentAction(const DNRHeaderAction& next_action) const { |
| DCHECK_EQ(header_info->header, next_action.header_info->header); |
| |
| switch (header_info->operation) { |
| case dnr_api::HeaderOperation::kAppend: |
| return next_action.header_info->operation != |
| dnr_api::HeaderOperation::kAppend; |
| case dnr_api::HeaderOperation::kSet: |
| return *extension_id != *next_action.extension_id || |
| next_action.header_info->operation != |
| dnr_api::HeaderOperation::kAppend; |
| case dnr_api::HeaderOperation::kRemove: |
| return true; |
| case dnr_api::HeaderOperation::kNone: |
| NOTREACHED(); |
| return true; |
| } |
| } |
| |
| // Non-owning pointers to HeaderInfo and ExtensionId. |
| raw_ptr<const DNRRequestAction::HeaderInfo> header_info; |
| raw_ptr<const extensions::ExtensionId, DanglingUntriaged> extension_id; |
| }; |
| |
| // Helper to modify request headers from |
| // |request_action.request_headers_to_modify|. Returns whether or not request |
| // headers were actually modified and modifies |removed_headers|, |set_headers| |
| // and |header_actions|. |header_actions| maps a header name to the operation |
| // to be performed on the header. |
| bool ModifyRequestHeadersForAction( |
| net::HttpRequestHeaders* headers, |
| const DNRRequestAction& request_action, |
| std::set<std::string>* removed_headers, |
| std::set<std::string>* set_headers, |
| std::map<std::string_view, std::vector<DNRHeaderAction>>* header_actions) { |
| bool request_headers_modified = false; |
| for (const DNRRequestAction::HeaderInfo& header_info : |
| request_action.request_headers_to_modify) { |
| bool header_modified = false; |
| const std::string& header = header_info.header; |
| |
| DNRHeaderAction header_action(&header_info, &request_action.extension_id); |
| auto iter = header_actions->find(header); |
| |
| // Checking the first DNRHeaderAction should suffice for determining if a |
| // conflict exists, since the contents of |header_actions| for a given |
| // header will always be one of: |
| // [remove] |
| // [append+] one or more appends |
| // [set, append*] set, any number of appends from the same extension |
| // This is enforced in ConflictsWithSubsequentAction by checking the |
| // operation type of the subsequent action against the first action. |
| if (iter != header_actions->end() && |
| iter->second[0].ConflictsWithSubsequentAction(header_action)) { |
| continue; |
| } |
| auto& actions_for_header = (*header_actions)[header]; |
| actions_for_header.push_back(header_action); |
| |
| switch (header_info.operation) { |
| case dnr_api::HeaderOperation::kAppend: |
| case dnr_api::HeaderOperation::kSet: { |
| DCHECK(header_info.value.has_value()); |
| bool has_header = headers->HasHeader(header); |
| headers->SetHeader(header, GetDNRNewRequestHeaderValue( |
| headers, header, *header_info.value, |
| header_info.operation)); |
| header_modified = true; |
| set_headers->insert(header); |
| |
| // Record only the first time a header is changed by a DNR action, which |
| // means only one action (this one) is currently in |header_actions| for |
| // this header, Each header should only contribute one count into the |
| // histogram as the count represents the total number of headers that |
| // have been changed by DNR actions. |
| if (actions_for_header.size() == 1) { |
| if (has_header) |
| RecordRequestHeader(header, &RecordDNRRequestHeaderChanged); |
| else |
| RecordRequestHeader(header, &RecordDNRRequestHeaderAdded); |
| } |
| break; |
| } |
| case dnr_api::HeaderOperation::kRemove: { |
| while (headers->HasHeader(header)) { |
| header_modified = true; |
| headers->RemoveHeader(header); |
| } |
| |
| if (header_modified) { |
| removed_headers->insert(header); |
| RecordRequestHeader(header, &RecordDNRRequestHeaderRemoved); |
| } |
| break; |
| } |
| case dnr_api::HeaderOperation::kNone: |
| NOTREACHED(); |
| } |
| |
| request_headers_modified |= header_modified; |
| } |
| |
| return request_headers_modified; |
| } |
| |
| // Helper to modify response headers from |request_action|. Returns whether or |
| // not response headers were actually modified and modifies |header_actions|. |
| // |header_actions| maps a header name to a list of operations to be performed |
| // on the header. |
| bool ModifyResponseHeadersForAction( |
| const net::HttpResponseHeaders* original_response_headers, |
| scoped_refptr<net::HttpResponseHeaders>* override_response_headers, |
| const DNRRequestAction& request_action, |
| std::map<std::string_view, std::vector<DNRHeaderAction>>* header_actions) { |
| bool response_headers_modified = false; |
| |
| // Check for |header| in |override_response_headers| if headers have been |
| // modified, otherwise, check in |original_response_headers|. |
| auto has_header = [&original_response_headers, |
| &override_response_headers](std::string header) { |
| return override_response_headers->get() |
| ? override_response_headers->get()->HasHeader(header) |
| : original_response_headers->HasHeader(header); |
| }; |
| |
| // Create a copy of |original_response_headers| iff we really want to modify |
| // the response headers. |
| auto create_override_headers_if_needed = |
| [&original_response_headers]( |
| scoped_refptr<net::HttpResponseHeaders>* override_response_headers) { |
| if (!override_response_headers->get()) { |
| *override_response_headers = |
| base::MakeRefCounted<net::HttpResponseHeaders>( |
| original_response_headers->raw_headers()); |
| } |
| }; |
| |
| for (const DNRRequestAction::HeaderInfo& header_info : |
| request_action.response_headers_to_modify) { |
| bool header_modified = false; |
| const std::string& header = header_info.header; |
| |
| DNRHeaderAction header_action(&header_info, &request_action.extension_id); |
| auto iter = header_actions->find(header); |
| |
| // Checking the first DNRHeaderAction should suffice for determining if a |
| // conflict exists, since the contents of |header_actions| for a given |
| // header will always be one of: |
| // [remove] |
| // [append+] one or more appends |
| // [set, append*] set, any number of appends from the same extension |
| // This is enforced in ConflictsWithSubsequentAction by checking the |
| // operation type of the subsequent action against the first action. |
| if (iter != header_actions->end() && |
| iter->second[0].ConflictsWithSubsequentAction(header_action)) { |
| continue; |
| } |
| auto& actions_for_header = (*header_actions)[header]; |
| actions_for_header.push_back(header_action); |
| |
| switch (header_info.operation) { |
| case dnr_api::HeaderOperation::kRemove: { |
| if (has_header(header)) { |
| header_modified = true; |
| create_override_headers_if_needed(override_response_headers); |
| override_response_headers->get()->RemoveHeader(header); |
| RecordResponseHeader(header, &RecordDNRResponseHeaderRemoved); |
| } |
| |
| break; |
| } |
| case dnr_api::HeaderOperation::kAppend: { |
| header_modified = true; |
| create_override_headers_if_needed(override_response_headers); |
| override_response_headers->get()->AddHeader(header, *header_info.value); |
| |
| // Record only the first time a header is appended. appends following a |
| // set from the same extension are treated as part of the set and are |
| // not logged. |
| if (actions_for_header.size() == 1) |
| RecordResponseHeader(header, &RecordDNRResponseHeaderAdded); |
| |
| break; |
| } |
| case dnr_api::HeaderOperation::kSet: { |
| header_modified = true; |
| create_override_headers_if_needed(override_response_headers); |
| override_response_headers->get()->RemoveHeader(header); |
| override_response_headers->get()->AddHeader(header, *header_info.value); |
| RecordResponseHeader(header, &RecordDNRResponseHeaderChanged); |
| break; |
| } |
| case dnr_api::HeaderOperation::kNone: |
| NOTREACHED(); |
| } |
| |
| response_headers_modified |= header_modified; |
| } |
| |
| return response_headers_modified; |
| } |
| |
| } // namespace |
| |
| IgnoredAction::IgnoredAction(extensions::ExtensionId extension_id, |
| web_request::IgnoredActionType action_type) |
| : extension_id(std::move(extension_id)), action_type(action_type) {} |
| |
| IgnoredAction::IgnoredAction(IgnoredAction&& rhs) = default; |
| |
| bool ExtraInfoSpec::InitFromValue(content::BrowserContext* browser_context, |
| const base::Value& value, |
| int* extra_info_spec) { |
| *extra_info_spec = 0; |
| if (!value.is_list()) |
| return false; |
| for (const auto& item : value.GetList()) { |
| const std::string* str = item.GetIfString(); |
| if (!str) |
| return false; |
| |
| if (*str == "requestHeaders") { |
| *extra_info_spec |= REQUEST_HEADERS; |
| } else if (*str == "responseHeaders") { |
| *extra_info_spec |= RESPONSE_HEADERS; |
| } else if (*str == "blocking") { |
| *extra_info_spec |= BLOCKING; |
| } else if (*str == "asyncBlocking") { |
| *extra_info_spec |= ASYNC_BLOCKING; |
| } else if (*str == "requestBody") { |
| *extra_info_spec |= REQUEST_BODY; |
| } else if (*str == "extraHeaders") { |
| *extra_info_spec |= EXTRA_HEADERS; |
| } else { |
| return false; |
| } |
| } |
| // BLOCKING and ASYNC_BLOCKING are mutually exclusive. |
| if ((*extra_info_spec & BLOCKING) && (*extra_info_spec & ASYNC_BLOCKING)) |
| return false; |
| return true; |
| } |
| |
| RequestCookie::RequestCookie() = default; |
| RequestCookie::RequestCookie(RequestCookie&& other) = default; |
| RequestCookie& RequestCookie ::operator=(RequestCookie&& other) = default; |
| RequestCookie::~RequestCookie() = default; |
| |
| bool RequestCookie::operator==(const RequestCookie& other) const { |
| return std::tie(name, value) == std::tie(other.name, other.value); |
| } |
| |
| RequestCookie RequestCookie::Clone() const { |
| RequestCookie clone; |
| clone.name = name; |
| clone.value = value; |
| return clone; |
| } |
| |
| ResponseCookie::ResponseCookie() = default; |
| ResponseCookie::ResponseCookie(ResponseCookie&& other) = default; |
| ResponseCookie& ResponseCookie ::operator=(ResponseCookie&& other) = default; |
| ResponseCookie::~ResponseCookie() = default; |
| |
| bool ResponseCookie::operator==(const ResponseCookie& other) const { |
| return std::tie(name, value, expires, max_age, domain, path, secure, |
| http_only) == |
| std::tie(other.name, other.value, other.expires, other.max_age, |
| other.domain, other.path, other.secure, other.http_only); |
| } |
| |
| ResponseCookie ResponseCookie::Clone() const { |
| ResponseCookie clone; |
| clone.name = name; |
| clone.value = value; |
| clone.expires = expires; |
| clone.max_age = max_age; |
| clone.domain = domain; |
| clone.path = path; |
| clone.secure = secure; |
| clone.http_only = http_only; |
| return clone; |
| } |
| |
| FilterResponseCookie::FilterResponseCookie() = default; |
| FilterResponseCookie::FilterResponseCookie(FilterResponseCookie&& other) = |
| default; |
| FilterResponseCookie& FilterResponseCookie ::operator=( |
| FilterResponseCookie&& other) = default; |
| FilterResponseCookie::~FilterResponseCookie() = default; |
| |
| bool FilterResponseCookie::operator==(const FilterResponseCookie& other) const { |
| // This ignores all of the fields of the base class ResponseCookie. Why? |
| // https://crbug.com/916248 |
| return std::tie(age_lower_bound, age_upper_bound, session_cookie) == |
| std::tie(other.age_lower_bound, other.age_upper_bound, |
| other.session_cookie); |
| } |
| |
| FilterResponseCookie FilterResponseCookie::Clone() const { |
| FilterResponseCookie clone; |
| clone.name = name; |
| clone.value = value; |
| clone.expires = expires; |
| clone.max_age = max_age; |
| clone.domain = domain; |
| clone.path = path; |
| clone.secure = secure; |
| clone.http_only = http_only; |
| clone.age_upper_bound = age_upper_bound; |
| clone.age_lower_bound = age_lower_bound; |
| clone.session_cookie = session_cookie; |
| return clone; |
| } |
| |
| RequestCookieModification::RequestCookieModification() = default; |
| RequestCookieModification::RequestCookieModification( |
| RequestCookieModification&& other) = default; |
| RequestCookieModification& RequestCookieModification ::operator=( |
| RequestCookieModification&& other) = default; |
| RequestCookieModification::~RequestCookieModification() = default; |
| |
| bool RequestCookieModification::operator==( |
| const RequestCookieModification& other) const { |
| // This ignores |type|. Why? https://crbug.com/916248 |
| return std::tie(filter, modification) == |
| std::tie(other.filter, other.modification); |
| } |
| |
| RequestCookieModification RequestCookieModification::Clone() const { |
| RequestCookieModification clone; |
| clone.type = type; |
| if (filter.has_value()) |
| clone.filter = filter->Clone(); |
| if (modification.has_value()) |
| clone.modification = modification->Clone(); |
| return clone; |
| } |
| |
| ResponseCookieModification::ResponseCookieModification() : type(ADD) {} |
| ResponseCookieModification::ResponseCookieModification( |
| ResponseCookieModification&& other) = default; |
| ResponseCookieModification& ResponseCookieModification ::operator=( |
| ResponseCookieModification&& other) = default; |
| ResponseCookieModification::~ResponseCookieModification() = default; |
| |
| bool ResponseCookieModification::operator==( |
| const ResponseCookieModification& other) const { |
| // This ignores |type|. Why? https://crbug.com/916248 |
| return std::tie(filter, modification) == |
| std::tie(other.filter, other.modification); |
| } |
| |
| ResponseCookieModification ResponseCookieModification::Clone() const { |
| ResponseCookieModification clone; |
| clone.type = type; |
| if (filter.has_value()) |
| clone.filter = filter->Clone(); |
| if (modification.has_value()) |
| clone.modification = modification->Clone(); |
| return clone; |
| } |
| |
| EventResponseDelta::EventResponseDelta( |
| const extensions::ExtensionId& extension_id, |
| const base::Time& extension_install_time) |
| : extension_id(extension_id), |
| extension_install_time(extension_install_time), |
| cancel(false) {} |
| |
| EventResponseDelta::EventResponseDelta(EventResponseDelta&& other) = default; |
| EventResponseDelta& EventResponseDelta ::operator=(EventResponseDelta&& other) = |
| default; |
| |
| EventResponseDelta::~EventResponseDelta() = default; |
| |
| bool InDecreasingExtensionInstallationTimeOrder(const EventResponseDelta& a, |
| const EventResponseDelta& b) { |
| return a.extension_install_time > b.extension_install_time; |
| } |
| |
| base::Value::List StringToCharList(const std::string& s) { |
| base::Value::List result; |
| for (const auto& c : s) |
| result.Append(*reinterpret_cast<const unsigned char*>(&c)); |
| return result; |
| } |
| |
| bool CharListToString(const base::Value::List& list, std::string* out) { |
| const size_t list_length = list.size(); |
| out->resize(list_length); |
| int value = 0; |
| for (size_t i = 0; i < list_length; ++i) { |
| if (!list[i].is_int()) |
| return false; |
| value = list[i].GetInt(); |
| if (value < 0 || value > 255) |
| return false; |
| unsigned char tmp = static_cast<unsigned char>(value); |
| (*out)[i] = *reinterpret_cast<char*>(&tmp); |
| } |
| return true; |
| } |
| |
| EventResponseDelta CalculateOnBeforeRequestDelta( |
| const extensions::ExtensionId& extension_id, |
| const base::Time& extension_install_time, |
| bool cancel, |
| const GURL& new_url) { |
| EventResponseDelta result(extension_id, extension_install_time); |
| result.cancel = cancel; |
| result.new_url = new_url; |
| return result; |
| } |
| |
| EventResponseDelta CalculateOnBeforeSendHeadersDelta( |
| content::BrowserContext* browser_context, |
| const extensions::ExtensionId& extension_id, |
| const base::Time& extension_install_time, |
| bool cancel, |
| net::HttpRequestHeaders* old_headers, |
| net::HttpRequestHeaders* new_headers, |
| int extra_info_spec) { |
| EventResponseDelta result(extension_id, extension_install_time); |
| result.cancel = cancel; |
| |
| // The event listener might not have passed any new headers if it |
| // just wanted to cancel the request. |
| if (new_headers) { |
| // Find deleted headers. |
| { |
| net::HttpRequestHeaders::Iterator i(*old_headers); |
| while (i.GetNext()) { |
| if (ShouldHideRequestHeader(browser_context, extra_info_spec, |
| i.name())) { |
| continue; |
| } |
| if (!new_headers->HasHeader(i.name())) { |
| result.deleted_request_headers.push_back(i.name()); |
| } |
| } |
| } |
| |
| // Find modified headers. |
| { |
| net::HttpRequestHeaders::Iterator i(*new_headers); |
| while (i.GetNext()) { |
| if (ShouldHideRequestHeader(browser_context, extra_info_spec, |
| i.name())) { |
| continue; |
| } |
| std::string value; |
| if (!old_headers->GetHeader(i.name(), &value) || i.value() != value) { |
| result.modified_request_headers.SetHeader(i.name(), i.value()); |
| } |
| } |
| } |
| } |
| return result; |
| } |
| |
| EventResponseDelta CalculateOnHeadersReceivedDelta( |
| const extensions::ExtensionId& extension_id, |
| const base::Time& extension_install_time, |
| bool cancel, |
| const GURL& old_url, |
| const GURL& new_url, |
| const net::HttpResponseHeaders* old_response_headers, |
| ResponseHeaders* new_response_headers, |
| int extra_info_spec) { |
| EventResponseDelta result(extension_id, extension_install_time); |
| result.cancel = cancel; |
| result.new_url = new_url; |
| |
| if (!new_response_headers) |
| return result; |
| |
| extensions::ExtensionsAPIClient* api_client = |
| extensions::ExtensionsAPIClient::Get(); |
| |
| // Find deleted headers (header keys are treated case insensitively). |
| { |
| size_t iter = 0; |
| std::string name; |
| std::string value; |
| while (old_response_headers->EnumerateHeaderLines(&iter, &name, &value)) { |
| if (api_client->ShouldHideResponseHeader(old_url, name)) |
| continue; |
| if (ShouldHideResponseHeader(extra_info_spec, name)) |
| continue; |
| bool header_found = false; |
| for (const auto& i : *new_response_headers) { |
| if (base::EqualsCaseInsensitiveASCII(i.first, name) && |
| value == i.second) { |
| header_found = true; |
| break; |
| } |
| } |
| if (!header_found) |
| result.deleted_response_headers.push_back(ResponseHeader(name, value)); |
| } |
| } |
| |
| // Find added headers (header keys are treated case insensitively). |
| { |
| for (const auto& i : *new_response_headers) { |
| if (api_client->ShouldHideResponseHeader(old_url, i.first)) |
| continue; |
| if (ShouldHideResponseHeader(extra_info_spec, i.first)) |
| continue; |
| size_t iter = 0; |
| std::string name; |
| std::string value; |
| bool header_found = false; |
| while (old_response_headers->EnumerateHeaderLines(&iter, &name, &value)) { |
| if (base::EqualsCaseInsensitiveASCII(name, i.first) && |
| value == i.second) { |
| header_found = true; |
| break; |
| } |
| } |
| if (!header_found) |
| result.added_response_headers.push_back(i); |
| } |
| } |
| |
| return result; |
| } |
| |
| EventResponseDelta CalculateOnAuthRequiredDelta( |
| const extensions::ExtensionId& extension_id, |
| const base::Time& extension_install_time, |
| bool cancel, |
| std::optional<net::AuthCredentials> auth_credentials) { |
| EventResponseDelta result(extension_id, extension_install_time); |
| result.cancel = cancel; |
| result.auth_credentials = std::move(auth_credentials); |
| return result; |
| } |
| |
| void MergeCancelOfResponses( |
| const EventResponseDeltas& deltas, |
| std::optional<extensions::ExtensionId>* canceled_by_extension) { |
| *canceled_by_extension = std::nullopt; |
| for (const auto& delta : deltas) { |
| if (delta.cancel) { |
| *canceled_by_extension = delta.extension_id; |
| break; |
| } |
| } |
| } |
| |
| // Helper function for MergeRedirectUrlOfResponses() that allows ignoring |
| // all redirects but those to data:// urls and about:blank. This is important |
| // to treat these URLs as "cancel urls", i.e. URLs that extensions redirect |
| // to if they want to express that they want to cancel a request. This reduces |
| // the number of conflicts that we need to flag, as canceling is considered |
| // a higher precedence operation that redirects. |
| // Returns whether a redirect occurred. |
| static bool MergeRedirectUrlOfResponsesHelper( |
| const GURL& url, |
| const EventResponseDeltas& deltas, |
| GURL* new_url, |
| IgnoredActions* ignored_actions, |
| bool consider_only_cancel_scheme_urls) { |
| // Redirecting WebSocket handshake request is prohibited. |
| if (url.SchemeIsWSOrWSS()) |
| return false; |
| |
| bool redirected = false; |
| |
| for (const auto& delta : deltas) { |
| if (!delta.new_url.is_valid()) { |
| continue; |
| } |
| if (consider_only_cancel_scheme_urls && |
| !delta.new_url.SchemeIs(url::kDataScheme) && |
| delta.new_url.spec() != "about:blank") { |
| continue; |
| } |
| |
| if (!redirected || *new_url == delta.new_url) { |
| *new_url = delta.new_url; |
| redirected = true; |
| } else { |
| ignored_actions->emplace_back(delta.extension_id, |
| web_request::IgnoredActionType::kRedirect); |
| } |
| } |
| return redirected; |
| } |
| |
| void MergeRedirectUrlOfResponses(const GURL& url, |
| const EventResponseDeltas& deltas, |
| GURL* new_url, |
| IgnoredActions* ignored_actions) { |
| // First handle only redirects to data:// URLs and about:blank. These are a |
| // special case as they represent a way of cancelling a request. |
| if (MergeRedirectUrlOfResponsesHelper(url, deltas, new_url, ignored_actions, |
| true)) { |
| // If any extension cancelled a request by redirecting to a data:// URL or |
| // about:blank, we don't consider the other redirects. |
| return; |
| } |
| |
| // Handle all other redirects. |
| MergeRedirectUrlOfResponsesHelper(url, deltas, new_url, ignored_actions, |
| false); |
| } |
| |
| void MergeOnBeforeRequestResponses(const GURL& url, |
| const EventResponseDeltas& deltas, |
| GURL* new_url, |
| IgnoredActions* ignored_actions) { |
| MergeRedirectUrlOfResponses(url, deltas, new_url, ignored_actions); |
| } |
| |
| static bool DoesRequestCookieMatchFilter( |
| const ParsedRequestCookie& cookie, |
| const std::optional<RequestCookie>& filter) { |
| if (!filter.has_value()) |
| return true; |
| if (filter->name.has_value() && cookie.first != *filter->name) |
| return false; |
| if (filter->value.has_value() && cookie.second != *filter->value) |
| return false; |
| return true; |
| } |
| |
| // Applies all CookieModificationType::ADD operations for request cookies of |
| // |deltas| to |cookies|. Returns whether any cookie was added. |
| static bool MergeAddRequestCookieModifications( |
| const EventResponseDeltas& deltas, |
| ParsedRequestCookies* cookies) { |
| bool modified = false; |
| // We assume here that the deltas are sorted in decreasing extension |
| // precedence (i.e. decreasing extension installation time). |
| for (const auto& delta : base::Reversed(deltas)) { |
| const RequestCookieModifications& modifications = |
| delta.request_cookie_modifications; |
| for (auto mod = modifications.cbegin(); mod != modifications.cend(); |
| ++mod) { |
| if (mod->type != ADD || !mod->modification.has_value()) |
| continue; |
| |
| if (!mod->modification->name.has_value() || |
| !mod->modification->value.has_value()) |
| continue; |
| |
| const std::string& new_name = *mod->modification->name; |
| const std::string& new_value = *mod->modification->value; |
| |
| bool cookie_with_same_name_found = false; |
| for (auto cookie = cookies->begin(); |
| cookie != cookies->end() && !cookie_with_same_name_found; ++cookie) { |
| if (cookie->first == new_name) { |
| if (cookie->second != new_value) { |
| cookie->second = new_value; |
| modified = true; |
| } |
| cookie_with_same_name_found = true; |
| } |
| } |
| if (!cookie_with_same_name_found) { |
| cookies->emplace_back(new_name, new_value); |
| modified = true; |
| } |
| } |
| } |
| return modified; |
| } |
| |
| // Applies all CookieModificationType::EDIT operations for request cookies of |
| // |deltas| to |cookies|. Returns whether any cookie was modified. |
| static bool MergeEditRequestCookieModifications( |
| const EventResponseDeltas& deltas, |
| ParsedRequestCookies* cookies) { |
| bool modified = false; |
| // We assume here that the deltas are sorted in decreasing extension |
| // precedence (i.e. decreasing extension installation time). |
| for (const auto& delta : base::Reversed(deltas)) { |
| const RequestCookieModifications& modifications = |
| delta.request_cookie_modifications; |
| for (auto mod = modifications.cbegin(); mod != modifications.cend(); |
| ++mod) { |
| if (mod->type != EDIT || !mod->modification.has_value()) |
| continue; |
| |
| if (!mod->modification->value.has_value()) |
| continue; |
| |
| const std::string& new_value = *mod->modification->value; |
| const std::optional<RequestCookie>& filter = mod->filter; |
| for (auto cookie = cookies->begin(); cookie != cookies->end(); ++cookie) { |
| if (!DoesRequestCookieMatchFilter(*cookie, filter)) |
| continue; |
| // If the edit operation tries to modify the cookie name, we just ignore |
| // this. We only modify the cookie value. |
| if (cookie->second != new_value) { |
| cookie->second = new_value; |
| modified = true; |
| } |
| } |
| } |
| } |
| return modified; |
| } |
| |
| // Applies all CookieModificationType::REMOVE operations for request cookies of |
| // |deltas| to |cookies|. Returns whether any cookie was deleted. |
| static bool MergeRemoveRequestCookieModifications( |
| const EventResponseDeltas& deltas, |
| ParsedRequestCookies* cookies) { |
| bool modified = false; |
| // We assume here that the deltas are sorted in decreasing extension |
| // precedence (i.e. decreasing extension installation time). |
| for (const auto& delta : base::Reversed(deltas)) { |
| const RequestCookieModifications& modifications = |
| delta.request_cookie_modifications; |
| for (auto mod = modifications.cbegin(); mod != modifications.cend(); |
| ++mod) { |
| if (mod->type != REMOVE) |
| continue; |
| |
| const std::optional<RequestCookie>& filter = mod->filter; |
| auto i = cookies->begin(); |
| while (i != cookies->end()) { |
| if (DoesRequestCookieMatchFilter(*i, filter)) { |
| i = cookies->erase(i); |
| modified = true; |
| } else { |
| ++i; |
| } |
| } |
| } |
| } |
| return modified; |
| } |
| |
| void MergeCookiesInOnBeforeSendHeadersResponses( |
| const GURL& url, |
| const EventResponseDeltas& deltas, |
| net::HttpRequestHeaders* request_headers) { |
| // Skip all work if there are no registered cookie modifications. |
| bool cookie_modifications_exist = false; |
| for (const auto& delta : deltas) { |
| cookie_modifications_exist |= !delta.request_cookie_modifications.empty(); |
| } |
| if (!cookie_modifications_exist) |
| return; |
| |
| // Parse old cookie line. |
| std::string cookie_header; |
| request_headers->GetHeader(net::HttpRequestHeaders::kCookie, &cookie_header); |
| ParsedRequestCookies cookies; |
| net::cookie_util::ParseRequestCookieLine(cookie_header, &cookies); |
| |
| // Modify cookies. |
| bool modified = false; |
| modified |= MergeAddRequestCookieModifications(deltas, &cookies); |
| modified |= MergeEditRequestCookieModifications(deltas, &cookies); |
| modified |= MergeRemoveRequestCookieModifications(deltas, &cookies); |
| |
| // Reassemble and store new cookie line. |
| if (modified) { |
| std::string new_cookie_header = |
| net::cookie_util::SerializeRequestCookieLine(cookies); |
| request_headers->SetHeader(net::HttpRequestHeaders::kCookie, |
| new_cookie_header); |
| } |
| } |
| |
| void MergeOnBeforeSendHeadersResponses( |
| const extensions::WebRequestInfo& request, |
| const EventResponseDeltas& deltas, |
| net::HttpRequestHeaders* request_headers, |
| IgnoredActions* ignored_actions, |
| std::set<std::string>* removed_headers, |
| std::set<std::string>* set_headers, |
| bool* request_headers_modified, |
| std::vector<const DNRRequestAction*>* matched_dnr_actions) { |
| DCHECK(request_headers_modified); |
| DCHECK(removed_headers->empty()); |
| DCHECK(set_headers->empty()); |
| DCHECK(request.dnr_actions); |
| DCHECK(matched_dnr_actions); |
| *request_headers_modified = false; |
| |
| std::map<std::string_view, std::vector<DNRHeaderAction>> dnr_header_actions; |
| for (const auto& action : *request.dnr_actions) { |
| bool headers_modified_for_action = |
| ModifyRequestHeadersForAction(request_headers, action, removed_headers, |
| set_headers, &dnr_header_actions); |
| |
| *request_headers_modified |= headers_modified_for_action; |
| if (headers_modified_for_action) |
| matched_dnr_actions->push_back(&action); |
| } |
| |
| // A strict subset of |removed_headers| consisting of headers removed by the |
| // web request API. Used for metrics. |
| // TODO(crbug.com/1098945): Use std::string_view to avoid copying header |
| // names. |
| std::set<std::string> web_request_removed_headers; |
| |
| // Subsets of |set_headers| consisting of headers modified by the web request |
| // API. Split into a set for added headers and a set for overridden headers. |
| std::set<std::string> web_request_overridden_headers; |
| std::set<std::string> web_request_added_headers; |
| |
| // We assume here that the deltas are sorted in decreasing extension |
| // precedence (i.e. decreasing extension installation time). |
| for (const auto& delta : deltas) { |
| if (delta.modified_request_headers.IsEmpty() && |
| delta.deleted_request_headers.empty()) { |
| continue; |
| } |
| |
| // Check whether any modification affects a request header that |
| // has been modified differently before. As deltas is sorted by decreasing |
| // extension installation order, this takes care of precedence. |
| bool extension_conflicts = false; |
| { |
| net::HttpRequestHeaders::Iterator modification( |
| delta.modified_request_headers); |
| while (modification.GetNext() && !extension_conflicts) { |
| // This modification sets |key| to |value|. |
| const std::string key = base::ToLowerASCII(modification.name()); |
| const std::string& value = modification.value(); |
| |
| // We must not modify anything that was specified to be removed by the |
| // Declarative Net Request API. Note that the actual header |
| // modifications made by Declarative Net Request should be represented |
| // in |removed_headers| and |set_headers|. |
| auto iter = dnr_header_actions.find(key); |
| if (iter != dnr_header_actions.end() && |
| iter->second[0].header_info->operation == |
| dnr_api::HeaderOperation::kRemove) { |
| extension_conflicts = true; |
| break; |
| } |
| |
| // We must not modify anything that has been deleted before. |
| if (base::Contains(*removed_headers, key)) { |
| extension_conflicts = true; |
| break; |
| } |
| |
| // We must not modify anything that has been set to a *different* |
| // value before. |
| if (base::Contains(*set_headers, key)) { |
| std::string current_value; |
| if (!request_headers->GetHeader(key, ¤t_value) || |
| current_value != value) { |
| extension_conflicts = true; |
| break; |
| } |
| } |
| } |
| } |
| |
| // Check whether any deletion affects a request header that has been |
| // modified before. |
| { |
| for (const std::string& key : delta.deleted_request_headers) { |
| if (base::Contains(*set_headers, base::ToLowerASCII(key))) { |
| extension_conflicts = true; |
| break; |
| } |
| } |
| } |
| |
| // Now execute the modifications if there were no conflicts. |
| if (!extension_conflicts) { |
| // Populate |set_headers|, |overridden_headers| and |added_headers| and |
| // perform the modifications. |
| net::HttpRequestHeaders::Iterator modification( |
| delta.modified_request_headers); |
| while (modification.GetNext()) { |
| std::string key = base::ToLowerASCII(modification.name()); |
| if (!request_headers->HasHeader(key)) { |
| web_request_added_headers.insert(key); |
| } else if (!base::Contains(web_request_added_headers, key)) { |
| // Note: |key| will only be present in |added_headers| if this is an |
| // identical edit. |
| web_request_overridden_headers.insert(key); |
| } |
| |
| set_headers->insert(key); |
| |
| request_headers->SetHeader(key, modification.value()); |
| } |
| |
| // Perform all deletions and record which keys were deleted. |
| { |
| for (const auto& header : delta.deleted_request_headers) { |
| std::string lowercase_header = base::ToLowerASCII(header); |
| |
| request_headers->RemoveHeader(header); |
| removed_headers->insert(lowercase_header); |
| web_request_removed_headers.insert(lowercase_header); |
| } |
| } |
| *request_headers_modified = true; |
| } else { |
| ignored_actions->emplace_back( |
| delta.extension_id, web_request::IgnoredActionType::kRequestHeaders); |
| } |
| } |
| |
| auto record_request_headers = [](const std::set<std::string>& headers, |
| void (*record_func)(RequestHeaderType)) { |
| if (headers.empty()) { |
| record_func(RequestHeaderType::kNone); |
| return; |
| } |
| for (const auto& header : headers) |
| RecordRequestHeader(header, record_func); |
| }; |
| |
| // Some sanity checks. |
| DCHECK(base::ranges::all_of(*removed_headers, IsStringLowerCaseASCII)); |
| DCHECK(base::ranges::all_of(*set_headers, IsStringLowerCaseASCII)); |
| DCHECK(base::ranges::includes( |
| *set_headers, |
| base::STLSetUnion<std::set<std::string>>( |
| web_request_added_headers, web_request_overridden_headers))); |
| DCHECK(base::STLSetIntersection<std::set<std::string>>( |
| web_request_added_headers, web_request_overridden_headers) |
| .empty()); |
| DCHECK(base::STLSetIntersection<std::set<std::string>>(*removed_headers, |
| *set_headers) |
| .empty()); |
| DCHECK(base::ranges::includes(*removed_headers, web_request_removed_headers)); |
| |
| // Record request header removals, additions and modifications. |
| record_request_headers(web_request_removed_headers, |
| &RecordRequestHeaderRemoved); |
| record_request_headers(web_request_added_headers, &RecordRequestHeaderAdded); |
| record_request_headers(web_request_overridden_headers, |
| &RecordRequestHeaderChanged); |
| |
| // Currently, conflicts are ignored while merging cookies. |
| MergeCookiesInOnBeforeSendHeadersResponses(request.url, deltas, |
| request_headers); |
| } |
| |
| // Retrieves all cookies from |override_response_headers|. |
| static ParsedResponseCookies GetResponseCookies( |
| scoped_refptr<net::HttpResponseHeaders> override_response_headers) { |
| ParsedResponseCookies result; |
| |
| size_t iter = 0; |
| std::string value; |
| while ( |
| override_response_headers->EnumerateHeader(&iter, "Set-Cookie", &value)) { |
| result.push_back(std::make_unique<net::ParsedCookie>(value)); |
| } |
| return result; |
| } |
| |
| // Stores all |cookies| in |override_response_headers| deleting previously |
| // existing cookie definitions. |
| static void StoreResponseCookies( |
| const ParsedResponseCookies& cookies, |
| scoped_refptr<net::HttpResponseHeaders> override_response_headers) { |
| override_response_headers->RemoveHeader("Set-Cookie"); |
| for (const std::unique_ptr<net::ParsedCookie>& cookie : cookies) { |
| override_response_headers->AddHeader("Set-Cookie", cookie->ToCookieLine()); |
| } |
| } |
| |
| // Modifies |cookie| according to |modification|. Each value that is set in |
| // |modification| is applied to |cookie|. |
| static bool ApplyResponseCookieModification(const ResponseCookie& modification, |
| net::ParsedCookie* cookie) { |
| bool modified = false; |
| if (modification.name.has_value()) |
| modified |= cookie->SetName(*modification.name); |
| if (modification.value.has_value()) |
| modified |= cookie->SetValue(*modification.value); |
| if (modification.expires.has_value()) |
| modified |= cookie->SetExpires(*modification.expires); |
| if (modification.max_age.has_value()) |
| modified |= cookie->SetMaxAge(base::NumberToString(*modification.max_age)); |
| if (modification.domain.has_value()) |
| modified |= cookie->SetDomain(*modification.domain); |
| if (modification.path.has_value()) |
| modified |= cookie->SetPath(*modification.path); |
| if (modification.secure.has_value()) |
| modified |= cookie->SetIsSecure(*modification.secure); |
| if (modification.http_only.has_value()) |
| modified |= cookie->SetIsHttpOnly(*modification.http_only); |
| return modified; |
| } |
| |
| static bool DoesResponseCookieMatchFilter( |
| const net::ParsedCookie& cookie, |
| const std::optional<FilterResponseCookie>& filter) { |
| if (!cookie.IsValid()) |
| return false; |
| if (!filter.has_value()) |
| return true; |
| if (filter->name && cookie.Name() != *filter->name) |
| return false; |
| if (filter->value && cookie.Value() != *filter->value) |
| return false; |
| if (filter->expires) { |
| std::string actual_value = |
| cookie.HasExpires() ? cookie.Expires() : std::string(); |
| if (actual_value != *filter->expires) |
| return false; |
| } |
| if (filter->max_age) { |
| std::string actual_value = |
| cookie.HasMaxAge() ? cookie.MaxAge() : std::string(); |
| if (actual_value != base::NumberToString(*filter->max_age)) |
| return false; |
| } |
| if (filter->domain) { |
| std::string actual_value = |
| cookie.HasDomain() ? cookie.Domain() : std::string(); |
| if (actual_value != *filter->domain) |
| return false; |
| } |
| if (filter->path) { |
| std::string actual_value = cookie.HasPath() ? cookie.Path() : std::string(); |
| if (actual_value != *filter->path) |
| return false; |
| } |
| if (filter->secure && cookie.IsSecure() != *filter->secure) |
| return false; |
| if (filter->http_only && cookie.IsHttpOnly() != *filter->http_only) |
| return false; |
| if (filter->age_upper_bound || filter->age_lower_bound || |
| (filter->session_cookie && *filter->session_cookie)) { |
| int64_t seconds_to_expiry; |
| bool lifetime_parsed = ParseCookieLifetime(cookie, &seconds_to_expiry); |
| if (filter->age_upper_bound && seconds_to_expiry > *filter->age_upper_bound) |
| return false; |
| if (filter->age_lower_bound && seconds_to_expiry < *filter->age_lower_bound) |
| return false; |
| if (filter->session_cookie && *filter->session_cookie && lifetime_parsed) |
| return false; |
| } |
| return true; |
| } |
| |
| // Applies all CookieModificationType::ADD operations for response cookies of |
| // |deltas| to |cookies|. Returns whether any cookie was added. |
| static bool MergeAddResponseCookieModifications( |
| const EventResponseDeltas& deltas, |
| ParsedResponseCookies* cookies) { |
| bool modified = false; |
| // We assume here that the deltas are sorted in decreasing extension |
| // precedence (i.e. decreasing extension installation time). |
| for (const auto& delta : base::Reversed(deltas)) { |
| const ResponseCookieModifications& modifications = |
| delta.response_cookie_modifications; |
| for (const auto& mod : modifications) { |
| if (mod.type != ADD || !mod.modification.has_value()) |
| continue; |
| |
| // Cookie names are not unique in response cookies so we always append |
| // and never override. |
| auto cookie = std::make_unique<net::ParsedCookie>(std::string()); |
| ApplyResponseCookieModification(mod.modification.value(), cookie.get()); |
| cookies->push_back(std::move(cookie)); |
| modified = true; |
| } |
| } |
| return modified; |
| } |
| |
| // Applies all CookieModificationType::EDIT operations for response cookies of |
| // |deltas| to |cookies|. Returns whether any cookie was modified. |
| static bool MergeEditResponseCookieModifications( |
| const EventResponseDeltas& deltas, |
| ParsedResponseCookies* cookies) { |
| bool modified = false; |
| // We assume here that the deltas are sorted in decreasing extension |
| // precedence (i.e. decreasing extension installation time). |
| for (const auto& delta : base::Reversed(deltas)) { |
| const ResponseCookieModifications& modifications = |
| delta.response_cookie_modifications; |
| for (const auto& mod : modifications) { |
| if (mod.type != EDIT || !mod.modification.has_value()) |
| continue; |
| |
| for (const std::unique_ptr<net::ParsedCookie>& cookie : *cookies) { |
| if (DoesResponseCookieMatchFilter(*cookie.get(), mod.filter)) { |
| modified |= ApplyResponseCookieModification(mod.modification.value(), |
| cookie.get()); |
| } |
| } |
| } |
| } |
| return modified; |
| } |
| |
| // Applies all CookieModificationType::REMOVE operations for response cookies of |
| // |deltas| to |cookies|. Returns whether any cookie was deleted. |
| static bool MergeRemoveResponseCookieModifications( |
| const EventResponseDeltas& deltas, |
| ParsedResponseCookies* cookies) { |
| bool modified = false; |
| // We assume here that the deltas are sorted in decreasing extension |
| // precedence (i.e. decreasing extension installation time). |
| for (const auto& delta : base::Reversed(deltas)) { |
| const ResponseCookieModifications& modifications = |
| delta.response_cookie_modifications; |
| for (auto mod = modifications.cbegin(); mod != modifications.cend(); |
| ++mod) { |
| if (mod->type != REMOVE) |
| continue; |
| |
| auto i = cookies->begin(); |
| while (i != cookies->end()) { |
| if (DoesResponseCookieMatchFilter(*i->get(), mod->filter)) { |
| i = cookies->erase(i); |
| modified = true; |
| } else { |
| ++i; |
| } |
| } |
| } |
| } |
| return modified; |
| } |
| |
| void MergeCookiesInOnHeadersReceivedResponses( |
| const GURL& url, |
| const EventResponseDeltas& deltas, |
| const net::HttpResponseHeaders* original_response_headers, |
| scoped_refptr<net::HttpResponseHeaders>* override_response_headers) { |
| // Skip all work if there are no registered cookie modifications. |
| bool cookie_modifications_exist = false; |
| for (const auto& delta : base::Reversed(deltas)) { |
| cookie_modifications_exist |= !delta.response_cookie_modifications.empty(); |
| } |
| |
| if (!cookie_modifications_exist) |
| return; |
| |
| // Only create a copy if we really want to modify the response headers. |
| if (override_response_headers->get() == nullptr) { |
| *override_response_headers = base::MakeRefCounted<net::HttpResponseHeaders>( |
| original_response_headers->raw_headers()); |
| } |
| |
| ParsedResponseCookies cookies = |
| GetResponseCookies(*override_response_headers); |
| |
| bool modified = false; |
| modified |= MergeAddResponseCookieModifications(deltas, &cookies); |
| modified |= MergeEditResponseCookieModifications(deltas, &cookies); |
| modified |= MergeRemoveResponseCookieModifications(deltas, &cookies); |
| |
| // Store new value. |
| if (modified) |
| StoreResponseCookies(cookies, *override_response_headers); |
| } |
| |
| // Converts the key of the (key, value) pair to lower case. |
| static ResponseHeader ToLowerCase(const ResponseHeader& header) { |
| return ResponseHeader(base::ToLowerASCII(header.first), header.second); |
| } |
| |
| void MergeOnHeadersReceivedResponses( |
| const extensions::WebRequestInfo& request, |
| const EventResponseDeltas& deltas, |
| const net::HttpResponseHeaders* original_response_headers, |
| scoped_refptr<net::HttpResponseHeaders>* override_response_headers, |
| GURL* preserve_fragment_on_redirect_url, |
| IgnoredActions* ignored_actions, |
| bool* response_headers_modified, |
| std::vector<const DNRRequestAction*>* matched_dnr_actions) { |
| DCHECK(response_headers_modified); |
| *response_headers_modified = false; |
| |
| DCHECK(request.dnr_actions); |
| DCHECK(matched_dnr_actions); |
| |
| std::map<std::string_view, std::vector<DNRHeaderAction>> dnr_header_actions; |
| for (const auto& action : *request.dnr_actions) { |
| bool headers_modified_for_action = ModifyResponseHeadersForAction( |
| original_response_headers, override_response_headers, action, |
| &dnr_header_actions); |
| |
| *response_headers_modified |= headers_modified_for_action; |
| if (headers_modified_for_action) |
| matched_dnr_actions->push_back(&action); |
| } |
| |
| // Here we collect which headers we have removed or added so far due to |
| // extensions of higher precedence. Header keys are always stored as |
| // lower case. |
| std::set<ResponseHeader> removed_headers; |
| std::set<ResponseHeader> added_headers; |
| |
| // We assume here that the deltas are sorted in decreasing extension |
| // precedence (i.e. decreasing extension installation time). |
| for (const auto& delta : deltas) { |
| if (delta.added_response_headers.empty() && |
| delta.deleted_response_headers.empty()) { |
| continue; |
| } |
| |
| // Only create a copy if we really want to modify the response headers. |
| if (override_response_headers->get() == nullptr) { |
| *override_response_headers = |
| base::MakeRefCounted<net::HttpResponseHeaders>( |
| original_response_headers->raw_headers()); |
| } |
| |
| // We consider modifications as pairs of (delete, add) operations. |
| // If a header is deleted twice by different extensions we assume that the |
| // intention was to modify it to different values and consider this a |
| // conflict. As deltas is sorted by decreasing extension installation order, |
| // this takes care of precedence. |
| bool extension_conflicts = false; |
| for (const ResponseHeader& header : delta.deleted_response_headers) { |
| ResponseHeader lowercase_header(ToLowerCase(header)); |
| if (base::Contains(removed_headers, lowercase_header) || |
| base::Contains(dnr_header_actions, lowercase_header.first)) { |
| extension_conflicts = true; |
| break; |
| } |
| } |
| |
| // Prevent extensions from adding any response header which was specified to |
| // be removed or set by the Declarative Net Request API. However, multiple |
| // appends are allowed. |
| if (!extension_conflicts) { |
| for (const ResponseHeader& header : delta.added_response_headers) { |
| ResponseHeader lowercase_header(ToLowerCase(header)); |
| |
| auto it = dnr_header_actions.find(lowercase_header.first); |
| if (it == dnr_header_actions.end()) |
| continue; |
| |
| // Multiple appends are allowed. |
| if (it->second[0].header_info->operation != |
| dnr_api::HeaderOperation::kAppend) { |
| extension_conflicts = true; |
| break; |
| } |
| } |
| } |
| |
| // Now execute the modifications if there were no conflicts. |
| if (!extension_conflicts) { |
| // Delete headers |
| { |
| for (const ResponseHeader& header : delta.deleted_response_headers) { |
| (*override_response_headers) |
| ->RemoveHeaderLine(header.first, header.second); |
| removed_headers.insert(ToLowerCase(header)); |
| } |
| } |
| |
| // Add headers. |
| { |
| for (const ResponseHeader& header : delta.added_response_headers) { |
| ResponseHeader lowercase_header(ToLowerCase(header)); |
| if (added_headers.find(lowercase_header) != added_headers.end()) |
| continue; |
| added_headers.insert(lowercase_header); |
| (*override_response_headers)->AddHeader(header.first, header.second); |
| } |
| } |
| *response_headers_modified = true; |
| } else { |
| ignored_actions->emplace_back( |
| delta.extension_id, web_request::IgnoredActionType::kResponseHeaders); |
| } |
| } |
| |
| // Currently, conflicts are ignored while merging cookies. |
| MergeCookiesInOnHeadersReceivedResponses(request.url, deltas, |
| original_response_headers, |
| override_response_headers); |
| |
| GURL new_url; |
| MergeRedirectUrlOfResponses(request.url, deltas, &new_url, ignored_actions); |
| if (new_url.is_valid()) { |
| // Only create a copy if we really want to modify the response headers. |
| if (override_response_headers->get() == nullptr) { |
| *override_response_headers = |
| base::MakeRefCounted<net::HttpResponseHeaders>( |
| original_response_headers->raw_headers()); |
| } |
| |
| RedirectRequestAfterHeadersReceived(new_url, **override_response_headers, |
| preserve_fragment_on_redirect_url); |
| } |
| |
| // Record metrics. |
| { |
| auto record_response_headers = [](const std::set<std::string_view>& headers, |
| void (*record_func)(ResponseHeaderType)) { |
| if (headers.empty()) { |
| record_func(ResponseHeaderType::kNone); |
| return; |
| } |
| |
| for (const auto& header : headers) { |
| RecordResponseHeader(header, record_func); |
| } |
| }; |
| |
| std::set<std::string_view> modified_header_names; |
| std::set<std::string_view> added_header_names; |
| std::set<std::string_view> removed_header_names; |
| |
| for (const ResponseHeader& header : added_headers) { |
| // Skip logging this header if this was subsequently removed by an |
| // extension. |
| if (!override_response_headers->get()->HasHeader(header.first)) |
| continue; |
| |
| if (original_response_headers->HasHeader(header.first)) |
| modified_header_names.insert(header.first); |
| else |
| added_header_names.insert(header.first); |
| } |
| |
| for (const ResponseHeader& header : removed_headers) { |
| if (!override_response_headers->get()->HasHeader(header.first)) |
| removed_header_names.insert(header.first); |
| else |
| modified_header_names.insert(header.first); |
| } |
| |
| DCHECK(base::ranges::all_of(modified_header_names, IsStringLowerCaseASCII)); |
| DCHECK(base::ranges::all_of(added_header_names, IsStringLowerCaseASCII)); |
| DCHECK(base::ranges::all_of(removed_header_names, IsStringLowerCaseASCII)); |
| |
| record_response_headers(modified_header_names, |
| &RecordResponseHeaderChanged); |
| record_response_headers(added_header_names, &RecordResponseHeaderAdded); |
| record_response_headers(removed_header_names, &RecordResponseHeaderRemoved); |
| } |
| } |
| |
| bool MergeOnAuthRequiredResponses(const EventResponseDeltas& deltas, |
| net::AuthCredentials* auth_credentials, |
| IgnoredActions* ignored_actions) { |
| CHECK(auth_credentials); |
| bool credentials_set = false; |
| |
| for (const auto& delta : deltas) { |
| if (!delta.auth_credentials.has_value()) |
| continue; |
| bool different = |
| auth_credentials->username() != delta.auth_credentials->username() || |
| auth_credentials->password() != delta.auth_credentials->password(); |
| if (credentials_set && different) { |
| ignored_actions->emplace_back( |
| delta.extension_id, web_request::IgnoredActionType::kAuthCredentials); |
| } else { |
| *auth_credentials = *delta.auth_credentials; |
| credentials_set = true; |
| } |
| } |
| return credentials_set; |
| } |
| |
| void ClearCacheOnNavigation() { |
| if (content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)) { |
| ClearCacheOnNavigationOnUI(); |
| } else { |
| content::GetUIThreadTaskRunner({})->PostTask( |
| FROM_HERE, base::BindOnce(&ClearCacheOnNavigationOnUI)); |
| } |
| } |
| |
| // Converts the |name|, |value| pair of a http header to a HttpHeaders |
| // dictionary. |
| base::Value::Dict CreateHeaderDictionary(const std::string& name, |
| const std::string& value) { |
| base::Value::Dict header; |
| header.Set(keys::kHeaderNameKey, name); |
| if (base::IsStringUTF8(value)) { |
| header.Set(keys::kHeaderValueKey, value); |
| } else { |
| header.Set(keys::kHeaderBinaryValueKey, StringToCharList(value)); |
| } |
| return header; |
| } |
| |
| bool ShouldHideRequestHeader(content::BrowserContext* browser_context, |
| int extra_info_spec, |
| const std::string& name) { |
| static constexpr auto kRequestHeaders = |
| base::MakeFixedFlatSet<std::string_view>({"accept-encoding", |
| "accept-language", "cookie", |
| "origin", "referer"}); |
| return !(extra_info_spec & ExtraInfoSpec::EXTRA_HEADERS) && |
| base::Contains(kRequestHeaders, base::ToLowerASCII(name)); |
| } |
| |
| bool ShouldHideResponseHeader(int extra_info_spec, const std::string& name) { |
| return !(extra_info_spec & ExtraInfoSpec::EXTRA_HEADERS) && |
| base::EqualsCaseInsensitiveASCII(name, "set-cookie"); |
| } |
| |
| void RedirectRequestAfterHeadersReceived( |
| const GURL& new_url, |
| net::HttpResponseHeaders& override_response_headers, |
| GURL* preserve_fragment_on_redirect_url) { |
| override_response_headers.ReplaceStatusLine("HTTP/1.1 302 Found"); |
| override_response_headers.SetHeader("Location", new_url.spec()); |
| // Prevent the original URL's fragment from being added to the new URL. |
| *preserve_fragment_on_redirect_url = new_url; |
| } |
| |
| } // namespace extension_web_request_api_helpers |