@@ -131,6 +131,8 @@ public class FilterHttpResource { | |||||
final List<AppMatcher> matchers = matcherDAO.findByAccountAndFqdnAndEnabled(accountUuid, fqdn); | final List<AppMatcher> matchers = matcherDAO.findByAccountAndFqdnAndEnabled(accountUuid, fqdn); | ||||
if (log.isDebugEnabled()) log.debug("findMatchers: found "+matchers.size()+" candidate matchers"); | if (log.isDebugEnabled()) log.debug("findMatchers: found "+matchers.size()+" candidate matchers"); | ||||
final List<AppMatcher> removeMatchers; | final List<AppMatcher> removeMatchers; | ||||
List<String> options = null; | |||||
List<String> selectors = null; | |||||
if (matchers.isEmpty()) { | if (matchers.isEmpty()) { | ||||
removeMatchers = Collections.emptyList(); | removeMatchers = Collections.emptyList(); | ||||
} else { | } else { | ||||
@@ -138,12 +140,23 @@ public class FilterHttpResource { | |||||
removeMatchers = new ArrayList<>(); | removeMatchers = new ArrayList<>(); | ||||
for (AppMatcher matcher : matchers) { | for (AppMatcher matcher : matchers) { | ||||
if (matcher.matches(uri)) { | if (matcher.matches(uri)) { | ||||
switch (ruleEngine.preprocess(filterRequest, req, request, caller, device, matcher.getUuid())) { | |||||
final FilterMatchResponse matchResponse = ruleEngine.preprocess(filterRequest, req, request, caller, device, matcher.getUuid()); | |||||
switch (matchResponse.getDecision()) { | |||||
case abort_ok: return ABORT_OK; | |||||
case abort_not_found: return ABORT_NOT_FOUND; | |||||
case no_match: | case no_match: | ||||
removeMatchers.add(matcher); | removeMatchers.add(matcher); | ||||
break; | break; | ||||
case abort_ok: return ABORT_OK; | |||||
case abort_not_found: return ABORT_NOT_FOUND; | |||||
case match: | |||||
if (matchResponse.hasOptions()) { | |||||
if (options == null) options = new ArrayList<>(); | |||||
options.addAll(matchResponse.getOptions()); | |||||
} | |||||
if (matchResponse.hasSelectors()) { | |||||
if (selectors == null) selectors = new ArrayList<>(); | |||||
selectors.addAll(matchResponse.getSelectors()); | |||||
} | |||||
break; | |||||
} | } | ||||
} | } | ||||
} | } | ||||
@@ -151,7 +164,12 @@ public class FilterHttpResource { | |||||
matchers.removeAll(removeMatchers); | matchers.removeAll(removeMatchers); | ||||
if (log.isDebugEnabled()) log.debug("findMatchers: after pre-processing, returning "+matchers.size()+" matchers"); | if (log.isDebugEnabled()) log.debug("findMatchers: after pre-processing, returning "+matchers.size()+" matchers"); | ||||
return new FilterMatchersResponse().setMatchers(matchers).setDevice(device.getUuid()); | |||||
final FilterMatchersResponse response = new FilterMatchersResponse(); | |||||
return response | |||||
.setMatchers(matchers) | |||||
.setDevice(device.getUuid()) | |||||
.setOptions(options) | |||||
.setSelectors(selectors); | |||||
} | } | ||||
@POST @Path(EP_APPLY+"/{requestId}") | @POST @Path(EP_APPLY+"/{requestId}") | ||||
@@ -0,0 +1,27 @@ | |||||
package bubble.resources.stream; | |||||
import bubble.rule.FilterMatchDecision; | |||||
import lombok.Getter; | |||||
import lombok.NoArgsConstructor; | |||||
import lombok.Setter; | |||||
import lombok.experimental.Accessors; | |||||
import java.util.List; | |||||
@NoArgsConstructor @Accessors(chain=true) | |||||
public class FilterMatchResponse { | |||||
public static final FilterMatchResponse NO_MATCH = new FilterMatchResponse().setDecision(FilterMatchDecision.no_match); | |||||
public static final FilterMatchResponse MATCH = new FilterMatchResponse().setDecision(FilterMatchDecision.match); | |||||
public static final FilterMatchResponse ABORT_NOT_FOUND = new FilterMatchResponse().setDecision(FilterMatchDecision.abort_not_found); | |||||
public static final FilterMatchResponse ABORT_OK = new FilterMatchResponse().setDecision(FilterMatchDecision.abort_ok); | |||||
@Getter @Setter private FilterMatchDecision decision; | |||||
@Getter @Setter private List<String> options; | |||||
public boolean hasOptions () { return options != null && !options.isEmpty(); } | |||||
@Getter @Setter private List<String> selectors; | |||||
public boolean hasSelectors () { return selectors != null && !selectors.isEmpty(); } | |||||
} |
@@ -16,5 +16,7 @@ public class FilterMatchersResponse { | |||||
@Getter @Setter private Integer abort; | @Getter @Setter private Integer abort; | ||||
@Getter @Setter private String device; | @Getter @Setter private String device; | ||||
@Getter @Setter private List<AppMatcher> matchers; | @Getter @Setter private List<AppMatcher> matchers; | ||||
@Getter @Setter private List<String> options; | |||||
@Getter @Setter private List<String> selectors; | |||||
} | } |
@@ -4,6 +4,7 @@ import bubble.model.account.Account; | |||||
import bubble.model.app.AppMatcher; | import bubble.model.app.AppMatcher; | ||||
import bubble.model.app.AppRule; | import bubble.model.app.AppRule; | ||||
import bubble.model.device.Device; | import bubble.model.device.Device; | ||||
import bubble.resources.stream.FilterMatchResponse; | |||||
import bubble.service.stream.AppRuleHarness; | import bubble.service.stream.AppRuleHarness; | ||||
import bubble.resources.stream.FilterMatchersRequest; | import bubble.resources.stream.FilterMatchersRequest; | ||||
import com.fasterxml.jackson.databind.JsonNode; | import com.fasterxml.jackson.databind.JsonNode; | ||||
@@ -32,13 +33,13 @@ public interface AppRuleDriver { | |||||
Account account, | Account account, | ||||
Device device) {} | Device device) {} | ||||
default PreprocessDecision preprocess(AppRuleHarness ruleHarness, | |||||
FilterMatchersRequest filter, | |||||
Account account, | |||||
Device device, | |||||
Request req, | |||||
ContainerRequest request) { | |||||
return PreprocessDecision.match; | |||||
default FilterMatchResponse preprocess(AppRuleHarness ruleHarness, | |||||
FilterMatchersRequest filter, | |||||
Account account, | |||||
Device device, | |||||
Request req, | |||||
ContainerRequest request) { | |||||
return FilterMatchResponse.MATCH; | |||||
} | } | ||||
default InputStream filterRequest(InputStream in) { | default InputStream filterRequest(InputStream in) { | ||||
@@ -4,13 +4,13 @@ import com.fasterxml.jackson.annotation.JsonCreator; | |||||
import static bubble.ApiConstants.enumFromString; | import static bubble.ApiConstants.enumFromString; | ||||
public enum PreprocessDecision { | |||||
public enum FilterMatchDecision { | |||||
no_match, // associated matcher should not be included in request processing | no_match, // associated matcher should not be included in request processing | ||||
match, // associated should be included in request processing | match, // associated should be included in request processing | ||||
abort_ok, // abort request processing, return empty 200 OK response to client | abort_ok, // abort request processing, return empty 200 OK response to client | ||||
abort_not_found; // abort request processing, return empty 404 Not Found response to client | abort_not_found; // abort request processing, return empty 404 Not Found response to client | ||||
@JsonCreator public static PreprocessDecision fromString (String v) { return enumFromString(PreprocessDecision.class, v); } | |||||
@JsonCreator public static FilterMatchDecision fromString (String v) { return enumFromString(FilterMatchDecision.class, v); } | |||||
} | } |
@@ -3,9 +3,9 @@ package bubble.rule.analytics; | |||||
import bubble.model.account.Account; | import bubble.model.account.Account; | ||||
import bubble.model.app.AppData; | import bubble.model.app.AppData; | ||||
import bubble.model.device.Device; | import bubble.model.device.Device; | ||||
import bubble.resources.stream.FilterMatchResponse; | |||||
import bubble.resources.stream.FilterMatchersRequest; | import bubble.resources.stream.FilterMatchersRequest; | ||||
import bubble.rule.AbstractAppRuleDriver; | import bubble.rule.AbstractAppRuleDriver; | ||||
import bubble.rule.PreprocessDecision; | |||||
import bubble.service.stream.AppRuleHarness; | import bubble.service.stream.AppRuleHarness; | ||||
import lombok.Getter; | import lombok.Getter; | ||||
import org.cobbzilla.wizard.cache.redis.RedisService; | import org.cobbzilla.wizard.cache.redis.RedisService; | ||||
@@ -30,12 +30,12 @@ public class TrafficAnalytics extends AbstractAppRuleDriver { | |||||
@Getter(lazy=true) private final RedisService recentTraffic = redis.prefixNamespace(RECENT_TRAFFIC_PREFIX); | @Getter(lazy=true) private final RedisService recentTraffic = redis.prefixNamespace(RECENT_TRAFFIC_PREFIX); | ||||
@Override public PreprocessDecision preprocess(AppRuleHarness ruleHarness, | |||||
FilterMatchersRequest filter, | |||||
Account account, | |||||
Device device, | |||||
Request req, | |||||
ContainerRequest request) { | |||||
@Override public FilterMatchResponse preprocess(AppRuleHarness ruleHarness, | |||||
FilterMatchersRequest filter, | |||||
Account account, | |||||
Device device, | |||||
Request req, | |||||
ContainerRequest request) { | |||||
final String app = ruleHarness.getRule().getApp(); | final String app = ruleHarness.getRule().getApp(); | ||||
final String site = ruleHarness.getMatcher().getSite(); | final String site = ruleHarness.getMatcher().getSite(); | ||||
final String fqdn = filter.getFqdn(); | final String fqdn = filter.getFqdn(); | ||||
@@ -43,7 +43,7 @@ public class TrafficAnalytics extends AbstractAppRuleDriver { | |||||
getRecentTraffic().set(now()+"_"+randomAlphanumeric(10), json(new TrafficRecord(filter, account, device, req)), EX, RECENT_TRAFFIC_EXPIRATION); | getRecentTraffic().set(now()+"_"+randomAlphanumeric(10), json(new TrafficRecord(filter, account, device, req)), EX, RECENT_TRAFFIC_EXPIRATION); | ||||
incr(account, device, app, site, fqdn, DATE_FORMAT_YYYY_MM_DD_HH.print(now())); | incr(account, device, app, site, fqdn, DATE_FORMAT_YYYY_MM_DD_HH.print(now())); | ||||
incr(account, null, app, site, fqdn, DATE_FORMAT_YYYY_MM_DD_HH.print(now())); | incr(account, null, app, site, fqdn, DATE_FORMAT_YYYY_MM_DD_HH.print(now())); | ||||
return PreprocessDecision.no_match; // we are done, don't need to look at/modify stream | |||||
return FilterMatchResponse.NO_MATCH; // we are done, don't need to look at/modify stream | |||||
} | } | ||||
// we use synchronized here but in a multi-node scenario this is not sufficient, we still have some risk | // we use synchronized here but in a multi-node scenario this is not sufficient, we still have some risk | ||||
@@ -1,39 +1,87 @@ | |||||
package bubble.rule.bblock; | package bubble.rule.bblock; | ||||
import bubble.model.account.Account; | import bubble.model.account.Account; | ||||
import bubble.model.app.AppMatcher; | |||||
import bubble.model.app.AppRule; | |||||
import bubble.model.device.Device; | import bubble.model.device.Device; | ||||
import bubble.resources.stream.FilterMatchResponse; | |||||
import bubble.resources.stream.FilterMatchersRequest; | import bubble.resources.stream.FilterMatchersRequest; | ||||
import bubble.rule.PreprocessDecision; | |||||
import bubble.rule.analytics.TrafficAnalytics; | import bubble.rule.analytics.TrafficAnalytics; | ||||
import bubble.rule.bblock.spec.BlockDecision; | |||||
import bubble.rule.bblock.spec.BlockList; | |||||
import bubble.rule.bblock.spec.BlockListSource; | |||||
import bubble.service.stream.AppRuleHarness; | import bubble.service.stream.AppRuleHarness; | ||||
import com.fasterxml.jackson.databind.JsonNode; | |||||
import lombok.extern.slf4j.Slf4j; | |||||
import org.glassfish.grizzly.http.server.Request; | import org.glassfish.grizzly.http.server.Request; | ||||
import org.glassfish.jersey.server.ContainerRequest; | import org.glassfish.jersey.server.ContainerRequest; | ||||
import java.io.InputStream; | |||||
import java.util.Map; | |||||
import java.util.concurrent.ConcurrentHashMap; | |||||
import java.util.concurrent.TimeUnit; | |||||
import static org.cobbzilla.util.daemon.ZillaRuntime.now; | import static org.cobbzilla.util.daemon.ZillaRuntime.now; | ||||
import static org.cobbzilla.util.daemon.ZillaRuntime.shortError; | |||||
import static org.cobbzilla.util.json.JsonUtil.json; | |||||
import static org.cobbzilla.util.time.TimeUtil.DATE_FORMAT_YYYY_MM_DD_HH; | import static org.cobbzilla.util.time.TimeUtil.DATE_FORMAT_YYYY_MM_DD_HH; | ||||
@Slf4j | |||||
public class BubbleBlock extends TrafficAnalytics { | public class BubbleBlock extends TrafficAnalytics { | ||||
@Override public PreprocessDecision preprocess(AppRuleHarness ruleHarness, | |||||
FilterMatchersRequest filter, | |||||
Account account, | |||||
Device device, | |||||
Request req, | |||||
ContainerRequest request) { | |||||
private BubbleBlockConfig bubbleBlockConfig; | |||||
private BlockList blockList = new BlockList(); | |||||
private static Map<String, BlockListSource> blockListCache = new ConcurrentHashMap<>(); | |||||
@Override public void init(JsonNode config, JsonNode userConfig, AppRule rule, AppMatcher matcher, Account account, Device device) { | |||||
super.init(config, userConfig, rule, matcher, account, device); | |||||
bubbleBlockConfig = json(json(config), BubbleBlockConfig.class); | |||||
for (String listUrl : bubbleBlockConfig.getBlockLists()) { | |||||
BlockListSource blockListSource = blockListCache.get(listUrl); | |||||
if (blockListSource == null || blockListSource.age() > TimeUnit.DAYS.toMillis(5)) { | |||||
try { | |||||
final BlockListSource newList = new BlockListSource() | |||||
.setUrl(listUrl) | |||||
.download(); | |||||
blockListCache.put(listUrl, newList); | |||||
blockListSource = newList; | |||||
} catch (Exception e) { | |||||
log.error("init: error downloading blocklist "+listUrl+": "+shortError(e)); | |||||
continue; | |||||
} | |||||
} | |||||
blockList.merge(blockListSource.getBlockList()); | |||||
} | |||||
} | |||||
@Override public FilterMatchResponse preprocess(AppRuleHarness ruleHarness, | |||||
FilterMatchersRequest filter, | |||||
Account account, | |||||
Device device, | |||||
Request req, | |||||
ContainerRequest request) { | |||||
final String app = ruleHarness.getRule().getApp(); | final String app = ruleHarness.getRule().getApp(); | ||||
final String site = ruleHarness.getMatcher().getSite(); | final String site = ruleHarness.getMatcher().getSite(); | ||||
final String fqdn = filter.getFqdn(); | final String fqdn = filter.getFqdn(); | ||||
if (shouldBlock(account, device, filter)) { | |||||
incr(account, device, app, site, fqdn, DATE_FORMAT_YYYY_MM_DD_HH.print(now())); | |||||
incr(account, null, app, site, fqdn, DATE_FORMAT_YYYY_MM_DD_HH.print(now())); | |||||
return PreprocessDecision.abort_not_found; // block this request | |||||
final BlockDecision decision = blockList.getDecision(filter.getFqdn(), filter.getUri()); | |||||
switch (decision.getDecisionType()) { | |||||
case block: | |||||
incr(account, device, app, site, fqdn, DATE_FORMAT_YYYY_MM_DD_HH.print(now())); | |||||
incr(account, null, app, site, fqdn, DATE_FORMAT_YYYY_MM_DD_HH.print(now())); | |||||
return FilterMatchResponse.ABORT_NOT_FOUND; // block this request | |||||
case allow: default: | |||||
return FilterMatchResponse.NO_MATCH; | |||||
case filter: | |||||
return decision.getFilterMatchResponse(); | |||||
} | } | ||||
return PreprocessDecision.no_match; // don't block, and don't process this matcher (we do not modify streams) | |||||
} | } | ||||
private boolean shouldBlock(Account account, Device device, FilterMatchersRequest filter) { | |||||
return false; | |||||
@Override public InputStream doFilterResponse(String requestId, InputStream in) { | |||||
// todo : insert selector-based block JS | |||||
return in; | |||||
} | } | ||||
} | } |
@@ -0,0 +1,12 @@ | |||||
package bubble.rule.bblock; | |||||
import lombok.Getter; | |||||
import lombok.NoArgsConstructor; | |||||
import lombok.Setter; | |||||
@NoArgsConstructor | |||||
public class BubbleBlockConfig { | |||||
@Getter @Setter private String[] blockLists; | |||||
} |
@@ -0,0 +1,59 @@ | |||||
package bubble.rule.bblock.spec; | |||||
import bubble.resources.stream.FilterMatchResponse; | |||||
import bubble.rule.FilterMatchDecision; | |||||
import lombok.Getter; | |||||
import lombok.NoArgsConstructor; | |||||
import lombok.Setter; | |||||
import lombok.experimental.Accessors; | |||||
import java.util.ArrayList; | |||||
import java.util.List; | |||||
import static org.cobbzilla.util.daemon.ZillaRuntime.die; | |||||
import static org.cobbzilla.util.daemon.ZillaRuntime.empty; | |||||
@NoArgsConstructor @Accessors(chain=true) | |||||
public class BlockDecision { | |||||
public static final BlockDecision BLOCK = new BlockDecision().setDecisionType(BlockDecisionType.block); | |||||
public static final BlockDecision ALLOW = new BlockDecision().setDecisionType(BlockDecisionType.allow); | |||||
@Getter @Setter BlockDecisionType decisionType = BlockDecisionType.allow; | |||||
@Getter @Setter List<String> selectors; | |||||
@Getter @Setter List<String> options; | |||||
public BlockDecision add(BlockSpec block) { | |||||
if (block.hasSelector()) { | |||||
if (selectors == null) selectors = new ArrayList<>(); | |||||
selectors.add(block.getSelector()); | |||||
} | |||||
if (block.hasOptions()) { | |||||
if (options == null) options = new ArrayList<>(); | |||||
options.addAll(block.getOptions()); | |||||
} | |||||
if (!empty(selectors) || !empty(options)) decisionType = BlockDecisionType.filter; | |||||
return this; | |||||
} | |||||
public FilterMatchDecision getFilterMatchDecision() { | |||||
switch (decisionType) { | |||||
case block: return FilterMatchDecision.abort_not_found; | |||||
case allow: return FilterMatchDecision.no_match; | |||||
case filter: return FilterMatchDecision.match; | |||||
} | |||||
return die("getFilterMatchDecision: invalid decisionType: "+decisionType); | |||||
} | |||||
public FilterMatchResponse getFilterMatchResponse() { | |||||
switch (decisionType) { | |||||
case block: return FilterMatchResponse.ABORT_NOT_FOUND; | |||||
case allow: return FilterMatchResponse.NO_MATCH; | |||||
case filter: return new FilterMatchResponse() | |||||
.setDecision(FilterMatchDecision.match) | |||||
.setOptions(getOptions()) | |||||
.setSelectors(getSelectors()); | |||||
} | |||||
return die("getFilterMatchResponse: invalid decisionType: "+decisionType); | |||||
} | |||||
} |
@@ -0,0 +1,13 @@ | |||||
package bubble.rule.bblock.spec; | |||||
import com.fasterxml.jackson.annotation.JsonCreator; | |||||
import static bubble.ApiConstants.enumFromString; | |||||
public enum BlockDecisionType { | |||||
block, allow, filter; | |||||
@JsonCreator public static BlockDecisionType fromString (String v) { return enumFromString(BlockDecisionType.class, v); } | |||||
} |
@@ -0,0 +1,55 @@ | |||||
package bubble.rule.bblock.spec; | |||||
import lombok.Getter; | |||||
import lombok.NoArgsConstructor; | |||||
import lombok.experimental.Accessors; | |||||
import lombok.extern.slf4j.Slf4j; | |||||
import java.util.*; | |||||
@NoArgsConstructor @Accessors(chain=true) @Slf4j | |||||
public class BlockList { | |||||
@Getter private Set<BlockSpec> blacklist = new HashSet<>(); | |||||
@Getter private Set<BlockSpec> whitelist = new HashSet<>(); | |||||
public void addToWhitelist(BlockSpec spec) { | |||||
whitelist.add(spec); | |||||
} | |||||
public void addToWhitelist(List<BlockSpec> specs) { | |||||
for (BlockSpec spec : specs) addToWhitelist(spec); | |||||
} | |||||
public void addToBlacklist(BlockSpec spec) { | |||||
blacklist.add(spec); | |||||
} | |||||
public void addToBlacklist(List<BlockSpec> specs) { | |||||
for (BlockSpec spec : specs) addToBlacklist(spec); | |||||
} | |||||
public void merge(BlockList other) { | |||||
for (BlockSpec allow : other.getWhitelist()) { | |||||
addToWhitelist(allow); | |||||
} | |||||
for (BlockSpec block : other.getBlacklist()) { | |||||
addToBlacklist(block); | |||||
} | |||||
} | |||||
public BlockDecision getDecision(String fqdn, String path) { | |||||
for (BlockSpec allow : whitelist) { | |||||
if (allow.matches(fqdn, path)) return BlockDecision.ALLOW; | |||||
} | |||||
final BlockDecision decision = new BlockDecision(); | |||||
for (BlockSpec block : blacklist) { | |||||
if (block.matches(fqdn, path)) { | |||||
if (!block.hasSelector()) return BlockDecision.BLOCK; | |||||
decision.add(block); | |||||
} | |||||
} | |||||
return decision; | |||||
} | |||||
} |
@@ -0,0 +1,55 @@ | |||||
package bubble.rule.bblock.spec; | |||||
import lombok.Getter; | |||||
import lombok.NoArgsConstructor; | |||||
import lombok.Setter; | |||||
import lombok.experimental.Accessors; | |||||
import lombok.extern.slf4j.Slf4j; | |||||
import org.cobbzilla.util.http.HttpUtil; | |||||
import java.io.BufferedReader; | |||||
import java.io.IOException; | |||||
import java.io.InputStreamReader; | |||||
import static org.cobbzilla.util.daemon.ZillaRuntime.*; | |||||
import static org.cobbzilla.util.daemon.ZillaRuntime.now; | |||||
@NoArgsConstructor @Accessors(chain=true) @Slf4j | |||||
public class BlockListSource { | |||||
@Getter @Setter private String url; | |||||
@Getter @Setter private String format; | |||||
@Getter @Setter private Long lastDownloaded; | |||||
public long age () { return lastDownloaded == null ? Long.MAX_VALUE : now() - lastDownloaded; } | |||||
@Getter @Setter private BlockList blockList = new BlockList(); | |||||
public BlockListSource download() throws IOException { | |||||
try (BufferedReader r = new BufferedReader(new InputStreamReader(HttpUtil.get(url)))) { | |||||
String line; | |||||
boolean firstLine = true; | |||||
while ( (line = r.readLine()) != null ) { | |||||
if (empty(line)) continue; | |||||
line = line.trim(); | |||||
if (firstLine && line.startsWith("[") && line.endsWith("]")) { | |||||
format = line.substring(1, line.length()-1); | |||||
} | |||||
firstLine = false; | |||||
if (line.startsWith("!")) continue; | |||||
try { | |||||
if (line.startsWith("@@")) { | |||||
blockList.addToWhitelist(BlockSpec.parse(line)); | |||||
} else { | |||||
blockList.addToBlacklist(BlockSpec.parse(line)); | |||||
} | |||||
} catch (Exception e) { | |||||
log.warn("download("+url+"): error parsing line (skipping due to "+shortError(e)+"): " + line); | |||||
} | |||||
} | |||||
} | |||||
lastDownloaded = now(); | |||||
return this; | |||||
} | |||||
} |
@@ -0,0 +1,76 @@ | |||||
package bubble.rule.bblock.spec; | |||||
import lombok.AllArgsConstructor; | |||||
import lombok.Getter; | |||||
import org.cobbzilla.util.string.StringUtil; | |||||
import java.util.ArrayList; | |||||
import java.util.List; | |||||
import java.util.regex.Pattern; | |||||
import static org.cobbzilla.util.daemon.ZillaRuntime.empty; | |||||
@AllArgsConstructor | |||||
public class BlockSpec { | |||||
@Getter private BlockSpecTarget target; | |||||
@Getter private List<String> options; | |||||
@Getter(lazy=true) private final Pattern domainPattern = Pattern.compile(target.getDomainRegex()); | |||||
public boolean hasOptions () { return options != null && !options.isEmpty(); } | |||||
@Getter private String selector; | |||||
public boolean hasSelector() { return !empty(selector); } | |||||
public boolean isBlanket() { return !hasOptions() && !hasSelector(); } | |||||
public static List<BlockSpec> parse(String line) { | |||||
line = line.trim(); | |||||
int optionStartPos = line.indexOf('$'); | |||||
int selectorStartPos = line.indexOf("##"); | |||||
// sanity check that selectorStartPos > optionStartPos -- $ may occur AFTER ## if the selector contains a regex | |||||
if (selectorStartPos != -1 && optionStartPos > selectorStartPos) optionStartPos = -1; | |||||
final List<BlockSpecTarget> targets; | |||||
final List<String> options; | |||||
final String selector; | |||||
if (optionStartPos == -1) { | |||||
if (selectorStartPos == -1) { | |||||
// no options, no selector, entire line is the target | |||||
targets = BlockSpecTarget.parse(line); | |||||
options = null; | |||||
selector = null; | |||||
} else { | |||||
// no options, but selector present. split into target + selector | |||||
targets = BlockSpecTarget.parse(line.substring(0, selectorStartPos)); | |||||
options = null; | |||||
selector = line.substring(selectorStartPos+1); | |||||
} | |||||
} else { | |||||
if (selectorStartPos == -1) { | |||||
// no selector, split into target + options | |||||
targets = BlockSpecTarget.parse(line.substring(0, optionStartPos)); | |||||
options = StringUtil.splitAndTrim(line.substring(optionStartPos+1), ","); | |||||
selector = null; | |||||
} else { | |||||
// all 3 elements present | |||||
targets = BlockSpecTarget.parse(line.substring(0, optionStartPos)); | |||||
options = StringUtil.splitAndTrim(line.substring(optionStartPos + 1, selectorStartPos), ","); | |||||
selector = line.substring(selectorStartPos+1); | |||||
} | |||||
} | |||||
final List<BlockSpec> specs = new ArrayList<>(); | |||||
for (BlockSpecTarget target : targets) specs.add(new BlockSpec(target, options, selector)); | |||||
return specs; | |||||
} | |||||
public boolean matches(String fqdn, String path) { | |||||
if (getDomainPattern().matcher(fqdn).find()) { | |||||
return true; | |||||
} | |||||
return false; | |||||
} | |||||
} |
@@ -0,0 +1,40 @@ | |||||
package bubble.rule.bblock.spec; | |||||
import lombok.Getter; | |||||
import lombok.NoArgsConstructor; | |||||
import lombok.Setter; | |||||
import lombok.experimental.Accessors; | |||||
import java.util.ArrayList; | |||||
import java.util.List; | |||||
import java.util.regex.Pattern; | |||||
@NoArgsConstructor @Accessors(chain=true) | |||||
public class BlockSpecTarget { | |||||
@Getter @Setter private String domainRegex; | |||||
@Getter @Setter private String regex; | |||||
public static List<BlockSpecTarget> parse(String data) { | |||||
final List<BlockSpecTarget> targets = new ArrayList<>(); | |||||
for (String part : data.split(",")) { | |||||
targets.add(parseTarget(part)); | |||||
} | |||||
return targets; | |||||
} | |||||
private static BlockSpecTarget parseTarget(String data) { | |||||
String domainRegex = null; | |||||
String regex = null; | |||||
if (data.startsWith("||")) { | |||||
final int caretPos = data.indexOf("^"); | |||||
if (caretPos != -1) { | |||||
domainRegex = ".*?"+Pattern.quote(data.substring(2, caretPos))+"$"; | |||||
} else { | |||||
} | |||||
} | |||||
return new BlockSpecTarget().setDomainRegex(domainRegex).setRegex(regex); | |||||
} | |||||
} |
@@ -9,9 +9,9 @@ import bubble.model.app.AppRule; | |||||
import bubble.model.app.RuleDriver; | import bubble.model.app.RuleDriver; | ||||
import bubble.model.device.Device; | import bubble.model.device.Device; | ||||
import bubble.resources.stream.FilterHttpRequest; | import bubble.resources.stream.FilterHttpRequest; | ||||
import bubble.resources.stream.FilterMatchResponse; | |||||
import bubble.resources.stream.FilterMatchersRequest; | import bubble.resources.stream.FilterMatchersRequest; | ||||
import bubble.rule.AppRuleDriver; | import bubble.rule.AppRuleDriver; | ||||
import bubble.rule.PreprocessDecision; | |||||
import bubble.server.BubbleConfiguration; | import bubble.server.BubbleConfiguration; | ||||
import lombok.Cleanup; | import lombok.Cleanup; | ||||
import lombok.Getter; | import lombok.Getter; | ||||
@@ -71,12 +71,12 @@ public class RuleEngineService { | |||||
@Autowired private RuleDriverDAO driverDAO; | @Autowired private RuleDriverDAO driverDAO; | ||||
@Autowired private BubbleConfiguration configuration; | @Autowired private BubbleConfiguration configuration; | ||||
public PreprocessDecision preprocess(FilterMatchersRequest filter, | |||||
Request req, | |||||
ContainerRequest request, | |||||
Account account, | |||||
Device device, | |||||
String matcherUuid) { | |||||
public FilterMatchResponse preprocess(FilterMatchersRequest filter, | |||||
Request req, | |||||
ContainerRequest request, | |||||
Account account, | |||||
Device device, | |||||
String matcherUuid) { | |||||
final AppRuleHarness ruleHarness = initRules(account, device, new String[]{ matcherUuid }).get(0); | final AppRuleHarness ruleHarness = initRules(account, device, new String[]{ matcherUuid }).get(0); | ||||
return ruleHarness.getDriver().preprocess(ruleHarness, filter, account, device, req, request); | return ruleHarness.getDriver().preprocess(ruleHarness, filter, account, device, req, request); | ||||
} | } | ||||
@@ -38,7 +38,7 @@ | |||||
"priority": -1000, | "priority": -1000, | ||||
"config": { | "config": { | ||||
"blockLists": [ | "blockLists": [ | ||||
"https://v.firebog.net/hosts/Easylist.txt" | |||||
] | ] | ||||
} | } | ||||
}], | }], | ||||
@@ -0,0 +1,14 @@ | |||||
package bubble.rule.bblock.spec; | |||||
import org.junit.Test; | |||||
import static org.junit.Assert.assertEquals; | |||||
public class BlockListTest { | |||||
@Test public void testBlanketBlock () throws Exception { | |||||
final BlockList blockList = new BlockList(); | |||||
blockList.addToBlacklist(BlockSpec.parse("||fredfiber.no^")); | |||||
assertEquals("expected block", BlockDecisionType.block, blockList.getDecision("fredfiber.no", "/somepath").getDecisionType()); | |||||
} | |||||
} |
@@ -1 +1 @@ | |||||
Subproject commit 0200670256919c7b11ce19b340c5cfb843551379 | |||||
Subproject commit 78b8da16659be214013288328f932509ed4c9224 |