Skip to content

Commit

Permalink
redirect-url filter option
Browse files Browse the repository at this point in the history
  • Loading branch information
ShivanKaul authored and antonok-edm committed Sep 16, 2021
1 parent 009cd7d commit e937043
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 33 deletions.
59 changes: 45 additions & 14 deletions src/blocker.rs
Expand Up @@ -19,6 +19,18 @@ pub struct BlockerOptions {
pub enable_optimizations: bool,
}

/// Determines what should be loaded instead of a particular network request if the request also
/// matched a blocking filter.
#[derive(Debug, Deserialize, Serialize, PartialEq, Clone)]
pub enum Redirection {
/// Redirect to a stub resource loaded from the blocker's resource library. The field contains
/// the body of the redirect to be injected.
Resource(String),
/// Redirect to a remote resource. The field contains the URL of the replacement resource to be
/// loaded. These will only occur if previously enabled in `ParseOptions`.
Url(String),
}

#[derive(Debug, Serialize)]
pub struct BlockerResult {
pub matched: bool,
Expand All @@ -32,16 +44,16 @@ pub struct BlockerResult {
/// behaviour between them: checking should stop instead of moving to the
/// next instance iff an `important` rule matched.
pub important: bool,
/// Iff the blocker matches a rule which has the `redirect` option, as per
/// [uBlock Origin's redirect syntax][1], the `redirect` is `Some`. The
/// `redirect` field contains the body of the redirect to be injected.
/// Specifies what to load instead of the original request, rather than
/// just blocking it outright. This can come from a filter with a `redirect`
/// or `redirect-rule` option, or also from a `redirect-url` option if
/// enabled in `ParseOptions`. See `Redirection` for further instructions
/// on how the inner data should be interpreted.
///
/// Note that the presence of a redirect does _not_ imply that the request
/// should be blocked. The `redirect-rule` option can produce a redirection
/// that's only applied if another blocking filter matches a request.
///
/// [1]: https://github.com/gorhill/uBlock/wiki/Static-filter-syntax#redirect
pub redirect: Option<String>,
pub redirect: Option<Redirection>,
/// Exception is `Some` when the blocker matched on an exception rule.
/// Effectively this means that there was a match, but the request should
/// not be blocked. It is a non-empty string if the blocker was initialized
Expand Down Expand Up @@ -219,24 +231,39 @@ impl Blocker {

let redirect_filters = self.redirects.check_all(request, &request_tokens, &NO_TAGS);

// Extract the highest priority redirect directive.
// So far, priority specifiers are not supported, which means:
// 1. Exceptions - can bail immediately if found
// 2. Redirect URLs
// 3. Redirect resources
let redirect_option = {
let mut redirect = None;
// (true, s) implies s is a URL.
// (false, s) implies s is the name of a resource to lookup.
let mut redirect: Option<(bool, &str)> = None;
for redirect_filter in redirect_filters {
if redirect_filter.is_exception() {
redirect = None;
break;
} else {
redirect = redirect_filter.redirect.as_ref();
} else if redirect_filter.is_redirect_url() {
// Unconditionally write to `redirect` - it's the highest priority option that
// does not break the loop.
redirect = redirect_filter.redirect.as_ref().map(|s| (true, s.as_str()));
} else if redirect.is_none() {
// Otherwise, only write to `redirect` if it hasn't already been set by a
// previous filter.
redirect = redirect_filter.redirect.as_ref().map(|s| (false, s.as_str()));
}
}
redirect
};

let redirect: Option<String> = redirect_option.as_ref().and_then(|redirect_identifier| {
// Only match redirects with matching resources
if let Some(resource) = self.resources.get_resource(redirect_identifier) {
let redirect: Option<Redirection> = redirect_option.and_then(|(is_url, redirect_identifier)| {
if is_url {
Some(Redirection::Url(redirect_identifier.to_string()))
} else if let Some(resource) = self.resources.get_resource(redirect_identifier) {
// Only match resource redirects if a matching resource exists
let data_url = format!("data:{};base64,{}", resource.content_type, &resource.data);
Some(data_url.trim().to_owned())
Some(Redirection::Resource(data_url.trim().to_owned()))
} else {
// It's acceptable to pass no redirection if no matching resource is loaded.
// TODO - it may be useful to return a status flag to indicate that this occurred.
Expand Down Expand Up @@ -326,7 +353,7 @@ impl Blocker {
let mut exceptions = Vec::with_capacity(network_filters.len() / 8);
// $important
let mut importants = Vec::with_capacity(200);
// $redirect
// $redirect and $redirect-url
let mut redirects = Vec::with_capacity(200);
// $tag=
let mut tagged_filters_all = Vec::with_capacity(200);
Expand Down Expand Up @@ -450,6 +477,9 @@ impl Blocker {
} else if filter.is_redirect() {
self.redirects.add_filter(filter);
Ok(())
} else if filter.is_redirect_url() {
self.redirects.add_filter(filter);
Ok(())
} else if filter.tag.is_some() {
self.tagged_filters_all.push(filter);
let tags_enabled = self.tags_enabled().into_iter().collect::<HashSet<_>>();
Expand Down Expand Up @@ -1721,6 +1751,7 @@ mod legacy_rule_parsing_tests {
assert!(vec_hashmap_len(&blocker.filters.filter_map) +
vec_hashmap_len(&blocker.importants.filter_map) +
vec_hashmap_len(&blocker.redirects.filter_map) +
vec_hashmap_len(&blocker.redirect_urls.filter_map) +
vec_hashmap_len(&blocker.csp.filter_map) >=
expectation.filters - expectation.duplicates, "Number of collected network filters does not match expectation");
}
Expand Down
5 changes: 3 additions & 2 deletions src/engine.rs
Expand Up @@ -278,6 +278,7 @@ impl Engine {
mod tests {
use super::*;
use crate::resources::{ResourceType, MimeType};
use crate::blocker::Redirection;

#[test]
fn tags_enable_adds_tags() {
Expand Down Expand Up @@ -487,7 +488,7 @@ mod tests {
// TODO - The failure to match here is considered acceptable for now, as it's part of a
// breaking change (minor version bump). However, the test should be updated at some point.
//assert!(matched_rule.matched, "Expected match for {}", url);
assert_eq!(matched_rule.redirect, Some("data:text/plain;base64,".to_owned()), "Expected redirect to contain resource");
assert_eq!(matched_rule.redirect, Some(Redirection::Resource("data:text/plain;base64,".to_owned())), "Expected redirect to contain resource");
}

#[test]
Expand Down Expand Up @@ -554,7 +555,7 @@ mod tests {
let url = "http://example.com/ad-banner.gif";
let matched_rule = engine.check_network_urls(url, "", "");
assert!(matched_rule.matched, "Expected match for {}", url);
assert_eq!(matched_rule.redirect, Some("data:text/plain;base64,".to_owned()), "Expected redirect to contain resource");
assert_eq!(matched_rule.redirect, Some(Redirection::Resource("data:text/plain;base64,".to_owned())), "Expected redirect to contain resource");
}

#[test]
Expand Down
89 changes: 72 additions & 17 deletions src/filters/network.rs
@@ -1,6 +1,7 @@
use regex::{Regex, RegexSet};
use serde::{Deserialize, Serialize};
use once_cell::sync::Lazy;
use crate::url_parser::parse_url;

use std::fmt;
use std::sync::{Arc, RwLock};
Expand All @@ -11,7 +12,7 @@ use crate::utils::Hash;

pub const TOKENS_BUFFER_SIZE: usize = 200;

#[derive(Debug, PartialEq)]
#[derive(Debug, PartialEq, Clone)]
pub enum NetworkFilterError {
FilterParseError,
BugValueNotNumeric,
Expand All @@ -25,6 +26,8 @@ pub enum NetworkFilterError {
NegatedDocument,
GenericHideWithoutException,
EmptyRedirection,
RedirectionUrlInvalid,
MultipleRedirections,
UnrecognisedOption,
NoRegex,
FullRegexUnsupported,
Expand All @@ -51,7 +54,7 @@ bitflags::bitflags! {
const FROM_HTTPS = 1 << 12;
const IS_IMPORTANT = 1 << 13;
const MATCH_CASE = 1 << 14;
const _FUZZY_MATCH = 1 << 15; // Unused
const IS_REDIRECT_URL = 1 << 15;
const THIRD_PARTY = 1 << 16;
const FIRST_PARTY = 1 << 17;
const _EXPLICIT_CANCEL = 1 << 26; // Unused
Expand Down Expand Up @@ -222,6 +225,7 @@ enum NetworkFilterOption {
Bug(u32),
Tag(String),
Redirect(String),
RedirectUrl(String),
Csp(Option<String>),
Generichide,
Document,
Expand All @@ -238,6 +242,35 @@ enum NetworkFilterOption {
Font(bool),
}

impl NetworkFilterOption {
pub fn is_content_type(&self) -> bool {
match self {
Self::Document
| Self::Image(..)
| Self::Media(..)
| Self::Object(..)
| Self::Other(..)
| Self::Ping(..)
| Self::Script(..)
| Self::Stylesheet(..)
| Self::Subdocument(..)
| Self::XmlHttpRequest(..)
| Self::Websocket(..)
| Self::Font(..) => true,
_ => false,
}

}

pub fn is_redirection(&self) -> bool {
match self {
Self::Redirect(..) => true,
Self::RedirectUrl(..) => true,
_ => false,
}
}
}

/// Abstract syntax representation of a network filter. This representation can fully specify the
/// string representation of a filter as written, with the exception of aliased options like `1p`
/// or `ghide`. This allows separation of concerns between parsing and interpretation.
Expand Down Expand Up @@ -348,6 +381,19 @@ fn parse_filter_options(raw_options: &str) -> Result<Vec<NetworkFilterOption>, N

NetworkFilterOption::Redirect(String::from(value))
}
("redirect-url", true) => return Err(NetworkFilterError::NegatedRedirection),
("redirect-url", false) => {
// Ignore this filter if no redirection resource is specified
if value.is_empty() {
return Err(NetworkFilterError::EmptyRedirection);
}
// Parse URL
let maybe_parsed_url = parse_url(&String::from(value));
if maybe_parsed_url.is_none() {
return Err(NetworkFilterError::RedirectionUrlInvalid)
}
NetworkFilterOption::RedirectUrl(String::from(value))
}
("csp", _) => NetworkFilterOption::Csp(if !value.is_empty() {
Some(String::from(value))
} else {
Expand Down Expand Up @@ -424,21 +470,22 @@ impl PartialOrd for NetworkFilter {

/// Ensure that no invalid option combinations were provided for a filter.
fn validate_options(options: &[NetworkFilterOption]) -> Result<(), NetworkFilterError> {
use NetworkFilterOption as NfOpt;
// CSP options are incompatible with all content type options.
if options.iter().any(|e| matches!(e, NfOpt::Csp(..))) && options.iter().any(|e| matches!(e, NfOpt::Document
| NfOpt::Image(..)
| NfOpt::Media(..)
| NfOpt::Object(..)
| NfOpt::Other(..)
| NfOpt::Ping(..)
| NfOpt::Script(..)
| NfOpt::Stylesheet(..)
| NfOpt::Subdocument(..)
| NfOpt::XmlHttpRequest(..)
| NfOpt::Websocket(..)
| NfOpt::Font(..)
)) {
let mut has_csp = false;
let mut has_content_type = false;
let mut has_redirect = false;
for option in options {
if matches!(option, NetworkFilterOption::Csp(..)) {
has_csp = true;
} else if option.is_content_type() {
has_content_type = true;
} else if option.is_redirection() {
if has_redirect {
return Err(NetworkFilterError::MultipleRedirections);
}
has_redirect = true;
}
}
if has_csp && has_content_type {
return Err(NetworkFilterError::CspWithContentType);
}

Expand Down Expand Up @@ -528,6 +575,10 @@ impl NetworkFilter {
NetworkFilterOption::Bug(num) => bug = Some(num),
NetworkFilterOption::Tag(value) => tag = Some(value),
NetworkFilterOption::Redirect(value) => redirect = Some(value),
NetworkFilterOption::RedirectUrl(value) => redirect = {
mask.set(NetworkFilterMask::IS_REDIRECT_URL, true);
Some(value)
},
NetworkFilterOption::Csp(value) => {
mask.set(NetworkFilterMask::IS_CSP, true);
// CSP rules can never have content types, and should always match against
Expand Down Expand Up @@ -928,6 +979,10 @@ impl NetworkFilter {
self.redirect.is_some()
}

pub fn is_redirect_url(&self) -> bool {
self.redirect.is_some() && self.mask.contains(NetworkFilterMask::IS_REDIRECT_URL)
}

pub fn is_badfilter(&self) -> bool {
self.mask.contains(NetworkFilterMask::BAD_FILTER)
}
Expand Down

0 comments on commit e937043

Please sign in to comment.