From: csagan5 <32685696+csagan5@users.noreply.github.com> Date: Sat, 14 Sep 2019 10:14:54 +0200 Subject: Bromite AdBlock engine for SystemWebView --- android_webview/browser/net/aw_network_delegate.cc | 71 ++++ android_webview/browser/net/aw_network_delegate.h | 3 + net/BUILD.gn | 7 + net/url_request/adblock_intercept.cc | 389 +++++++++++++++++++++ net/url_request/adblock_intercept.h | 35 ++ 5 files changed, 505 insertions(+) create mode 100644 net/url_request/adblock_intercept.cc create mode 100644 net/url_request/adblock_intercept.h diff --git a/android_webview/browser/net/aw_network_delegate.cc b/android_webview/browser/net/aw_network_delegate.cc --- a/android_webview/browser/net/aw_network_delegate.cc +++ b/android_webview/browser/net/aw_network_delegate.cc @@ -20,9 +20,11 @@ #include "net/base/proxy_server.h" #include "net/http/http_response_headers.h" #include "net/proxy_resolution/proxy_info.h" +#include "net/url_request/adblock_intercept.h" #include "net/url_request/url_request.h" using content::BrowserThread; +using content::ResourceRequestInfo; namespace android_webview { @@ -49,6 +51,75 @@ AwNetworkDelegate::AwNetworkDelegate() {} AwNetworkDelegate::~AwNetworkDelegate() { } +#define TRANSPARENT1PXGIF "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" +#define EMPTYJS "data:text/javascript;base64,Cg==" +#define EMPTYCSS "data:text/css;base64,Cg==" + +static bool requestIntercepted(net::URLRequest* request, GURL* new_url) { + bool block = false, isValidUrl; + + // skip invalid URLs and browser-initiated requests (which have no initiator) + auto initiator = request->initiator(); + isValidUrl = request->url().is_valid() && initiator.has_value(); + std::string scheme = request->url().scheme(); + if (isValidUrl && scheme.length()) { + std::transform(scheme.begin(), scheme.end(), scheme.begin(), ::tolower); + if ("http" != scheme && "https" != scheme) { + isValidUrl = false; + } + } + ResourceRequestInfo* info; + if (isValidUrl) { + info = ResourceRequestInfo::ForRequest(request); + } + + // there are no per-site nor global ad/content settings when using the SystemWebView + bool adblock_enabled = true; + if (isValidUrl && info) { + auto resource_type = info->GetResourceType(); + + if (adblock_enabled + && content::ResourceType::kMainFrame != resource_type + && net::adblock_intercept(request->url(), initiator->host(), resource_type)) { + block = true; + } + + if (block) { + switch (resource_type) { + case content::ResourceType::kImage: + case content::ResourceType::kFavicon: + *new_url = GURL(TRANSPARENT1PXGIF); + break; + case content::ResourceType::kScript: + *new_url = GURL(EMPTYJS); + break; + case content::ResourceType::kStylesheet: + *new_url = GURL(EMPTYCSS); + break; + default: + *new_url = GURL(""); + return true; + } + } + } // valid URL and info + return false; +} +#undef TRANSPARENT1PXGIF +#undef EMPTYJS +#undef EMPTYCSS + +int AwNetworkDelegate::OnBeforeURLRequest(net::URLRequest* request, + net::CompletionOnceCallback callback, + GURL* new_url) { + if (request) { + // most requests will be modified rather than intercepted + if (requestIntercepted(request, new_url)) + return net::ERR_BLOCKED_BY_ADMINISTRATOR; + } // request + + return net::OK; +} + int AwNetworkDelegate::OnBeforeStartTransaction( net::URLRequest* request, net::CompletionOnceCallback callback, diff --git a/android_webview/browser/net/aw_network_delegate.h b/android_webview/browser/net/aw_network_delegate.h --- a/android_webview/browser/net/aw_network_delegate.h +++ b/android_webview/browser/net/aw_network_delegate.h @@ -22,6 +22,9 @@ class AwNetworkDelegate : public net::NetworkDelegateImpl { private: // NetworkDelegate implementation. + int OnBeforeURLRequest(net::URLRequest* request, + net::CompletionOnceCallback callback, + GURL* new_url) override; int OnBeforeStartTransaction(net::URLRequest* request, net::CompletionOnceCallback callback, net::HttpRequestHeaders* headers) override; diff --git a/net/BUILD.gn b/net/BUILD.gn --- a/net/BUILD.gn +++ b/net/BUILD.gn @@ -1842,6 +1842,13 @@ component("net") { "url_request/websocket_handshake_userdata_key.h", ] + if (is_android) { + sources += [ + "url_request/adblock_intercept.cc", + "url_request/adblock_intercept.h" + ] + } + if (enable_reporting) { sources += [ "network_error_logging/network_error_logging_service.cc", diff --git a/net/url_request/adblock_intercept.cc b/net/url_request/adblock_intercept.cc new file mode 100644 --- /dev/null +++ b/net/url_request/adblock_intercept.cc @@ -0,0 +1,389 @@ +/* + This file is part of Bromite. + + Bromite is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Bromite is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Bromite. If not, see . +*/ +#include "adblock_intercept.h" + +#ifdef ADB_TESTER +#include +#include +#include + +#include "log.h" +#include + +#else + +#include "net/base/registry_controlled_domains/registry_controlled_domain.h" +#include + +#endif + +#include "net/url_request/adblock_entries.h" + +namespace net { + +const char *LOG_TAG = "Bromite"; + +#ifdef ADB_TESTER +int adblock_rules_count() { return ADBLOCK_ENTRY_COUNT; } +#endif + +// True if the given canonical |host| is "[www.]." +// with a valid TLD. If |subdomain_permission| is ALLOW_SUBDOMAIN, we check +// against host "*.." instead. Will return the TLD +// string in |tld|, if specified and the |host| can be parsed. +static bool is_first_party(const char *l_host, int len, const char *l_url_host, + int b_len) { + size_t tld_length; + +#ifdef ADB_TESTER + char *found_tld; + + if (TLD_SUCCESS != tld_get_z(l_host, &found_tld)) + return false; + tld_length = strlen(found_tld); + if (tld_length == 0) + return false; +#else + tld_length = net::registry_controlled_domains::GetCanonicalHostRegistryLength( + l_host, net::registry_controlled_domains::EXCLUDE_UNKNOWN_REGISTRIES, + net::registry_controlled_domains::EXCLUDE_PRIVATE_REGISTRIES); + if ((tld_length == 0) || (tld_length == std::string::npos)) + return false; +#endif + + const char *tld = l_host + len - tld_length; + + // Removes any subdomain from origin host. + int i = len - tld_length - 2, top_i = i; + if (i < 0) { + return false; + } + const char *domain = l_host; + for (; i >= 0; i--) { + if (l_host[i] == '.') { + int p_len = top_i - i; + // skip "co" in "co.uk", "org" in "org.uk" + if (p_len <= 3) { + tld -= p_len + 1; + continue; + } + + // segment is long enough, accept it at as a domain + domain = l_host + i; + len -= i; + break; + } + } + +#ifdef ADBLOCK_LOG + __android_log_print(ANDROID_LOG_INFO, LOG_TAG, + "%s: extracted domain suffix: \"%s\" (TLD=\"%s\")", + l_host, domain, tld); +#endif + + // Check if supplied URL host matches, including the dot. + if (b_len < len) { + return false; + } + for (int i = 0; i < len; i++) { + if (l_url_host[b_len - 1 - i] != domain[len - 1 - i]) + return false; + } + + // pass with flying colors + return true; +} + +static char *strtolower(const char *str, int len, bool &should_free) { + char *ret = NULL; + for (int i = 0; i < len; i++) { + if ((65 <= str[i]) && (str[i] <= 90)) { + // first time a difference is found, allocate buffer and copy previous + // characters (if any) + if (ret == NULL) { + ret = (char *)malloc(len + 1); + if (i != 0) + memcpy(ret, str, i); + } + // convert character to lower case + ret[i] = str[i] + 32; + } else { + if (ret != NULL) + ret[i] = str[i]; + } + } + + if (ret != NULL) { + ret[len] = '\0'; + should_free = true; + return ret; + } + + // return source, unchanged + return (char *)str; +} + +static char *strtosep(const char *str, int len) { + char *ret = (char *)malloc(len + 3); + ret[0] = '^'; + for (int i = 0; i < len; i++) { + if ((str[i] == ':') || (str[i] == '/') || (str[i] == '?') || + (str[i] == '&') || (str[i] == '=')) { + ret[i + 1] = '^'; + } else { + ret[i + 1] = str[i]; + } + } + // the index 'len' character is set by the previous loop + ret[len + 1] = '^'; + ret[len + 2] = '\0'; + return ret; +} + +static bool url_matches(const char *c_url, char *c_url_sep, char *c_url_lower, + char *c_url_lower_sep, adblock_entry *entry) { + bool match = false; + // select comparison string based on case and separator presence (separator + // takes some shortcuts) + bool match_case = ((entry->flags & ADBLOCK_FLAG_MATCH_CASE) != 0); + bool match_separator = ((entry->flags & ADBLOCK_FLAG_HAS_SEPARATOR) != 0); + const char *match_url = + match_case ? (match_separator ? c_url_sep : c_url) + : (match_separator ? c_url_lower_sep : c_url_lower); + +#ifdef ADBLOCK_LOG_MORE + __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "[case:%d][sep:%d][%s]", + match_case, match_separator, match_url); +#endif + // check for all match parts at >= position of last match + const char *last = match_url; + for (int m = 0; const char *url_match = entry->matches[m]; m++) { + bool is_last_match = entry->matches[m + 1] == NULL; + const char *pos = strstr(last, url_match); + match = (pos != NULL); + +#ifdef ADBLOCK_LOG_MORE + __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "[%s][found:%d][match:%d]", + entry->matches[m], pos == NULL ? 0 : 1, match ? 1 : 0); +#endif + // check if the url starts with the first match part + if (match && (m == 0) && ((entry->flags & ADBLOCK_FLAG_MATCH_BEGIN) != 0) && + (pos != match_url)) + match = false; + + // check if the url ends with the last match part + if (match && is_last_match && + ((entry->flags & ADBLOCK_FLAG_MATCH_END) != 0) && + (pos != &match_url[strlen(match_url) - strlen(entry->matches[m])])) + match = false; + + // check domain match + if (match && (m == 0) && + ((entry->flags & ADBLOCK_FLAG_MATCH_DOMAIN) != 0) && + (pos != match_url) && (pos[-1] != '^') && (pos[-1] != '.') && + (pos[-1] != '/')) + match = false; + + // short circuit + if (!match) + break; + } + return match; +} + +bool url_match_domain(adblock_entry *entry, const std::string &origin_host) { + bool match_domain = true; + // check for a negative domain match + if (entry->domains_skip) { + if (origin_host.empty()) { + // skip this rule, cannot match on domain + return false; + } + for (int d = 0; const char *domain = entry->domains_skip[d]; d++) { + if (domain == origin_host) { + match_domain = false; + break; + } + } + } + + // check for a required positive domain match + if (entry->domains) { + if (origin_host.empty()) { + // skip this rule, cannot match on domain + return false; + } + for (int d = 0; const char *domain = entry->domains[d]; d++) { + if (domain != origin_host) { + match_domain = false; + break; + } + } + } + return match_domain; +} + +static bool url_match_party(adblock_entry *entry, const char *l_origin_host, + int origin_host_len, const char *l_url_host, + int url_host_len, bool &checked_fp, bool &fp) { + bool wanted_fp; + if ((entry->flags & ADBLOCK_FLAG_THIRD_PARTY) != 0) { + wanted_fp = false; + } else if ((entry->flags & ADBLOCK_FLAG_FIRST_PARTY) != 0) { + wanted_fp = true; + } else { + // no-op + return true; + } + + if (origin_host_len == 0) { + // cannot match this rule, no origin host to determine first/third party + return false; + } + + if (!checked_fp) { + // is the URL a first-party to the current page's host? + fp = is_first_party(l_origin_host, origin_host_len, l_url_host, + url_host_len); + + checked_fp = true; +#ifdef ADB_TESTER + __android_log_print(ANDROID_LOG_INFO, LOG_TAG, + "is_first_party(\"%s\", \"%s\") = %s", l_origin_host, + l_url_host, fp ? "true" : "false"); +#endif + } + + return fp == wanted_fp; +} + +static bool resource_type_match(adblock_entry *entry, + content::ResourceType resource_type) { + bool exclude; + if ((entry->flags & ADBLOCK_FLAG_RESOURCE_TYPE_IN) != 0) { + exclude = false; + } else if ((entry->flags & ADBLOCK_FLAG_RESOURCE_TYPE_NOT_IN) != 0) { + exclude = true; + } else { + // no resource type matching + return true; + } + + // use a bitwise trick to test for the current resource type + bool found = (entry->flags & (int(resource_type) << 16)); + if (!exclude) + return found; + return !found; +} + +int adblock_intercept(const GURL &url, const std::string &origin_host, + content::ResourceType resource_type) { + // these are verified on caller when not testing +#ifdef ADB_TESTER + if (!url.is_valid() || !url.SchemeIsHTTPOrHTTPS()) { + return 0; + } +#endif + + bool free1 = false, free2 = false, free3 = false, free4 = false; + int url_len = url.spec().size(); + const char *c_url = url.spec().c_str(); + char *c_url_lower = strtolower(c_url, url_len, free1); + char *c_url_sep = strtosep(c_url, url_len); + char *c_url_lower_sep = strtolower(c_url_sep, url_len + 2, free4); + + // might be empty in case of no host + const char *origin_host_cstr = origin_host.c_str(); + int origin_host_len = origin_host.size(), url_host_len = url.host().size(); + + // lower-case version of origin and url hosts + const char *c_origin_host_lower = + strtolower(origin_host_cstr, origin_host_len, free2), + *c_url_host_lower = + strtolower(url.host().c_str(), url_host_len, free3); + +#ifdef ADBLOCK_LOG + __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "[%s with host '%s'] [%s]", + c_url, url.host().c_str(), origin_host_cstr); +#endif + + bool checked_fp = false, fp = false; + + bool intercept = false; + for (int i = 0; i < ADBLOCK_ENTRY_COUNT; i++) { + adblock_entry *entry = &ADBLOCK_ENTRIES[i]; + + // no use checking rules when we're intercepting, or exceptions when not + bool check = + (!intercept && ((entry->flags & ADBLOCK_FLAG_EXCEPTION) == 0)) || + (intercept && ((entry->flags & ADBLOCK_FLAG_EXCEPTION) != 0)); + if (!check) + continue; + + // check for resource type + if (!resource_type_match(entry, resource_type)) + continue; + + // check for domain matches, a quick branch out if not matching + if (!url_match_domain(entry, origin_host)) + continue; + + // check on the URL matcher + if (!url_matches(c_url, c_url_sep, c_url_lower, c_url_lower_sep, entry)) + continue; + + // finally check first/third-party + if (!url_match_party(entry, c_origin_host_lower, origin_host_len, + c_url_host_lower, url_host_len, checked_fp, fp)) + continue; + +#ifdef ADBLOCK_LOG + if (!intercept) { + __android_log_print(ANDROID_LOG_INFO, LOG_TAG, + "--> intercept (#%d: \"%s\") (%x)", i, + entry->matches[0], entry->flags); + } else { + __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "--> pass (%d) (#%d)", i, + entry->flags); + } +#endif + intercept = !intercept; + } // for each entry + + if (free3) + free((void *)c_url_host_lower); + if (free2) + free((void *)c_origin_host_lower); + free(c_url_sep); + if (free1) + free(c_url_lower); + if (free4) + free(c_url_lower_sep); + + if (intercept) { +#ifdef ADBLOCK_LOG + __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "blocked"); +#endif + return 1; + } +#ifdef ADBLOCK_LOG + __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "pass"); +#endif + return 0; +} + +} // namespace net diff --git a/net/url_request/adblock_intercept.h b/net/url_request/adblock_intercept.h new file mode 100644 --- /dev/null +++ b/net/url_request/adblock_intercept.h @@ -0,0 +1,35 @@ +/* + This file is part of Bromite. + + Bromite is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Bromite is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Bromite. If not, see . +*/ + +#ifndef NET_URL_REQUEST_ADBLOCK_INTERCEPT_H_ +#define NET_URL_REQUEST_ADBLOCK_INTERCEPT_H_ + +#include "content/public/common/resource_type.h" +#include "url/gurl.h" + +namespace net { + +#ifdef ADB_TESTER +int adblock_rules_count(); +#endif + +int adblock_intercept(const GURL &url, const std::string &origin_host, + content::ResourceType resource_type); + +} // namespace net + +#endif // NET_URL_REQUEST_ADBLOCK_INTERCEPT_H_ -- 2.11.0