diff --git a/bubble-server/src/main/java/bubble/rule/AbstractAppRuleDriver.java b/bubble-server/src/main/java/bubble/rule/AbstractAppRuleDriver.java
index f20e7452..01901961 100644
--- a/bubble-server/src/main/java/bubble/rule/AbstractAppRuleDriver.java
+++ b/bubble-server/src/main/java/bubble/rule/AbstractAppRuleDriver.java
@@ -12,17 +12,35 @@ import bubble.model.account.Account;
import bubble.model.app.AppMatcher;
import bubble.model.app.AppRule;
import bubble.model.device.Device;
+import bubble.resources.stream.FilterHttpRequest;
import bubble.server.BubbleConfiguration;
import bubble.service.stream.AppPrimerService;
import com.fasterxml.jackson.databind.JsonNode;
import com.github.jknack.handlebars.Handlebars;
import lombok.Getter;
import lombok.Setter;
+import org.apache.commons.io.input.ReaderInputStream;
+import org.cobbzilla.util.collection.ExpirationMap;
+import org.cobbzilla.util.handlebars.HandlebarsUtil;
+import org.cobbzilla.util.io.FileUtil;
+import org.cobbzilla.util.io.regex.RegexFilterReader;
+import org.cobbzilla.util.io.regex.RegexReplacementFilter;
import org.cobbzilla.util.system.Bytes;
import org.cobbzilla.wizard.cache.redis.RedisService;
import org.springframework.beans.factory.annotation.Autowired;
+import java.io.File;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.HashMap;
+import java.util.Map;
+
+import static bubble.ApiConstants.HOME_DIR;
+import static org.cobbzilla.util.daemon.ZillaRuntime.die;
+import static org.cobbzilla.util.daemon.ZillaRuntime.empty;
+import static org.cobbzilla.util.io.regex.RegexReplacementFilter.DEFAULT_PREFIX_REPLACEMENT_WITH_MATCH;
import static org.cobbzilla.util.json.JsonUtil.json;
+import static org.cobbzilla.util.string.StringUtil.UTF8cs;
public abstract class AbstractAppRuleDriver implements AppRuleDriver {
@@ -77,4 +95,97 @@ public abstract class AbstractAppRuleDriver implements AppRuleDriver {
}
}
+ public static final String DEFAULT_INSERTION_REGEX = "<\\s*head[^>]*>";
+ public static final String DEFAULT_SCRIPT_OPEN = "";
+
+ protected static String insertionRegex (String customRegex) {
+ return empty(customRegex) ? DEFAULT_INSERTION_REGEX : customRegex;
+ }
+
+ protected static String scriptOpen (FilterHttpRequest filterRequest, String customNonceOpen, String customNoNonceOpen) {
+ return filterRequest.hasScriptNonce()
+ ? (empty(customNonceOpen) ? DEFAULT_SCRIPT_NONCE_OPEN : customNonceOpen).replace(NONCE_VAR, filterRequest.getScriptNonce())
+ : (empty(customNoNonceOpen) ? DEFAULT_SCRIPT_OPEN : customNoNonceOpen);
+ }
+
+ protected static String scriptClose (String customClose) {
+ return empty(customClose) ? DEFAULT_SCRIPT_CLOSE : customClose;
+ }
+
+ protected String getSiteJsTemplate (String defaultSiteTemplate) {
+ if (configuration.getEnvironment().containsKey("DEBUG_JS_SITE_TEMPLATES")) {
+ final File jsTemplateFile = new File(HOME_DIR + "/siteJsTemplates/" + requestModConfig().getSiteJsTemplate());
+ if (jsTemplateFile.exists()) {
+ return FileUtil.toStringOrDie(jsTemplateFile);
+ }
+ }
+ return defaultSiteTemplate;
+ }
+
+ private RequestModifierConfig requestModConfig() {
+ if (this instanceof RequestModifierRule) return ((RequestModifierRule) this).getRequestModifierConfig();
+ return die("requestModConfig: rule "+getClass().getName()+" does not implement RequestModifierRule");
+ }
+
+ @Getter(lazy=true) private final String insertionRegex = insertionRegex(requestModConfig().getInsertionRegex());
+
+ @Getter(lazy=true) private final String scriptClose = scriptClose(requestModConfig().getScriptClose());
+
+ protected InputStream filterInsertJs(InputStream in,
+ FilterHttpRequest filterRequest,
+ Map filterCtx,
+ String bubbleJsTemplate,
+ String defaultSiteTemplate,
+ String siteJsInsertionVar) {
+ final RequestModifierConfig modConfig = requestModConfig();
+ final String replacement = DEFAULT_PREFIX_REPLACEMENT_WITH_MATCH
+ + scriptOpen(filterRequest, modConfig.getScriptOpenNonce(), modConfig.getScriptOpenNoNonce())
+ + getBubbleJs(filterRequest.getId(), filterCtx, bubbleJsTemplate, defaultSiteTemplate, siteJsInsertionVar)
+ + getScriptClose();
+
+ final RegexReplacementFilter filter = new RegexReplacementFilter(getInsertionRegex(), replacement);
+ RegexFilterReader reader = new RegexFilterReader(new InputStreamReader(in), filter).setMaxMatches(1);
+ if (modConfig.hasAdditionalRegexReplacements()) {
+ for (BubbleRegexReplacement re : modConfig.getAdditionalRegexReplacements()) {
+ final RegexReplacementFilter f = new RegexReplacementFilter(re.getInsertionRegex(), re.getReplacement());
+ reader = new RegexFilterReader(reader, f);
+ }
+ }
+
+ return new ReaderInputStream(reader, UTF8cs);
+ }
+
+ protected String getBubbleJs(String requestId,
+ Map filterCtx,
+ String bubbleJsTemplate,
+ String defaultSiteTemplate,
+ String siteJsInsertionVar) {
+ final Map ctx = getBubbleJsContext(requestId, filterCtx);
+
+ if (!empty(siteJsInsertionVar) && !empty(defaultSiteTemplate)) {
+ final String siteJs = HandlebarsUtil.apply(getHandlebars(), getSiteJsTemplate(defaultSiteTemplate), ctx);
+ ctx.put(siteJsInsertionVar, siteJs);
+ }
+
+ return HandlebarsUtil.apply(getHandlebars(), bubbleJsTemplate, ctx);
+ }
+
+ protected Map getBubbleJsContext(String requestId, Map filterCtx) {
+ final Map ctx = new HashMap<>();
+ ctx.put(CTX_JS_PREFIX, AppRuleDriver.getJsPrefix(requestId));
+ ctx.put(CTX_BUBBLE_REQUEST_ID, requestId);
+ ctx.put(CTX_BUBBLE_HOME, configuration.getPublicUriBase());
+ ctx.put(CTX_SITE, getSiteName(matcher));
+ ctx.put(CTX_BUBBLE_DATA_ID, getDataId(requestId));
+ return ctx;
+ }
+
+ private static final ExpirationMap siteNameCache = new ExpirationMap<>();
+ protected String getSiteName(AppMatcher matcher) {
+ return siteNameCache.computeIfAbsent(matcher.getSite(), k -> appSiteDAO.findByAccountAndId(matcher.getAccount(), matcher.getSite()).getName());
+ }
+
}
diff --git a/bubble-server/src/main/java/bubble/rule/FilterMatchDecision.java b/bubble-server/src/main/java/bubble/rule/FilterMatchDecision.java
index fe2a8d29..5702adba 100644
--- a/bubble-server/src/main/java/bubble/rule/FilterMatchDecision.java
+++ b/bubble-server/src/main/java/bubble/rule/FilterMatchDecision.java
@@ -16,7 +16,7 @@ import static org.cobbzilla.util.http.HttpStatusCodes.OK;
public enum FilterMatchDecision {
no_match (OK), // associated matcher should not be included in request processing
- match (OK), // associated should be included in request processing
+ match (OK), // associated matcher should be included in request processing
abort_ok (OK), // abort request processing, return empty 200 OK response to client
abort_not_found (NOT_FOUND), // abort request processing, return empty 404 Not Found response to client
pass_thru (OK); // pass-through TLS request, do not intercept
@@ -26,4 +26,6 @@ public enum FilterMatchDecision {
@Getter private final int httpStatusCode;
public int httpStatus() { return getHttpStatusCode(); }
+ public boolean isAbort () { return this.name().startsWith("abort"); }
+
}
diff --git a/bubble-server/src/main/java/bubble/rule/RequestModifierConfig.java b/bubble-server/src/main/java/bubble/rule/RequestModifierConfig.java
new file mode 100644
index 00000000..781dd293
--- /dev/null
+++ b/bubble-server/src/main/java/bubble/rule/RequestModifierConfig.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2020 Bubble, Inc. All rights reserved.
+ * For personal (non-commercial) use, see license: https://getbubblenow.com/bubble-license/
+ */
+package bubble.rule;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.cobbzilla.util.collection.NameAndValue;
+
+import static org.cobbzilla.util.daemon.ZillaRuntime.empty;
+
+public class RequestModifierConfig {
+
+ @Getter @Setter private String siteJsTemplate;
+ @Getter @Setter private NameAndValue[] additionalJsTemplates;
+
+ @Getter @Setter private String insertionRegex;
+ @Getter @Setter private String scriptOpenNonce;
+ @Getter @Setter private String scriptOpenNoNonce;
+ @Getter @Setter private String scriptClose;
+
+ @Getter @Setter private BubbleRegexReplacement[] additionalRegexReplacements;
+ public boolean hasAdditionalRegexReplacements () { return !empty(additionalRegexReplacements); }
+
+}
diff --git a/bubble-server/src/main/java/bubble/rule/RequestModifierRule.java b/bubble-server/src/main/java/bubble/rule/RequestModifierRule.java
new file mode 100644
index 00000000..afb44c01
--- /dev/null
+++ b/bubble-server/src/main/java/bubble/rule/RequestModifierRule.java
@@ -0,0 +1,11 @@
+/**
+ * Copyright (c) 2020 Bubble, Inc. All rights reserved.
+ * For personal (non-commercial) use, see license: https://getbubblenow.com/bubble-license/
+ */
+package bubble.rule;
+
+public interface RequestModifierRule {
+
+ RequestModifierConfig getRequestModifierConfig ();
+
+}
diff --git a/bubble-server/src/main/java/bubble/rule/bblock/BubbleBlockConfig.java b/bubble-server/src/main/java/bubble/rule/bblock/BubbleBlockConfig.java
index ce1d34b7..b9a12875 100644
--- a/bubble-server/src/main/java/bubble/rule/bblock/BubbleBlockConfig.java
+++ b/bubble-server/src/main/java/bubble/rule/bblock/BubbleBlockConfig.java
@@ -4,6 +4,7 @@
*/
package bubble.rule.bblock;
+import bubble.rule.RequestModifierConfig;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@@ -14,13 +15,17 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import static org.cobbzilla.util.daemon.ZillaRuntime.bool;
import static org.cobbzilla.util.daemon.ZillaRuntime.empty;
@NoArgsConstructor @Slf4j
-public class BubbleBlockConfig {
+public class BubbleBlockConfig extends RequestModifierConfig {
@Getter @Setter private Boolean inPageBlocks;
- public boolean inPageBlocks() { return inPageBlocks != null && inPageBlocks; }
+ public boolean inPageBlocks() { return bool(inPageBlocks); }
+
+ @Getter @Setter private Boolean showStats;
+ public boolean showStats() { return bool(showStats); }
@Getter @Setter private BubbleUserAgentBlock[] userAgentBlocks;
public boolean hasUserAgentBlocks () { return !empty(userAgentBlocks); }
diff --git a/bubble-server/src/main/java/bubble/rule/bblock/BubbleBlockRuleDriver.java b/bubble-server/src/main/java/bubble/rule/bblock/BubbleBlockRuleDriver.java
index 7e4ac12d..3a741d93 100644
--- a/bubble-server/src/main/java/bubble/rule/bblock/BubbleBlockRuleDriver.java
+++ b/bubble-server/src/main/java/bubble/rule/bblock/BubbleBlockRuleDriver.java
@@ -11,25 +11,22 @@ import bubble.model.app.AppRule;
import bubble.model.device.Device;
import bubble.resources.stream.FilterHttpRequest;
import bubble.resources.stream.FilterMatchersRequest;
-import bubble.rule.AppRuleDriver;
import bubble.rule.FilterMatchDecision;
+import bubble.rule.RequestModifierConfig;
+import bubble.rule.RequestModifierRule;
import bubble.rule.analytics.TrafficAnalyticsRuleDriver;
import bubble.service.stream.AppRuleHarness;
import bubble.service.stream.ConnectionCheckResponse;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.io.input.ReaderInputStream;
-import org.cobbzilla.util.handlebars.HandlebarsUtil;
+import org.apache.commons.collections4.map.SingletonMap;
import org.cobbzilla.util.http.URIUtil;
-import org.cobbzilla.util.io.regex.RegexFilterReader;
-import org.cobbzilla.util.io.regex.RegexReplacementFilter;
import org.cobbzilla.util.string.StringUtil;
import org.glassfish.grizzly.http.server.Request;
import org.glassfish.jersey.server.ContainerRequest;
import java.io.IOException;
import java.io.InputStream;
-import java.io.InputStreamReader;
import java.net.URI;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@@ -43,11 +40,10 @@ import static org.cobbzilla.util.http.HttpContentTypes.isHtml;
import static org.cobbzilla.util.io.StreamUtil.stream2string;
import static org.cobbzilla.util.json.JsonUtil.COMPACT_MAPPER;
import static org.cobbzilla.util.json.JsonUtil.json;
-import static org.cobbzilla.util.string.StringUtil.UTF8cs;
import static org.cobbzilla.util.string.StringUtil.getPackagePath;
@Slf4j
-public class BubbleBlockRuleDriver extends TrafficAnalyticsRuleDriver {
+public class BubbleBlockRuleDriver extends TrafficAnalyticsRuleDriver implements RequestModifierRule {
private final AtomicReference blockList = new AtomicReference<>(new BlockList());
private BlockList getBlockList() { return blockList.get(); }
@@ -62,6 +58,13 @@ public class BubbleBlockRuleDriver extends TrafficAnalyticsRuleDriver {
@Override public Class getConfigClass() { return (Class) BubbleBlockConfig.class; }
+ @Override public RequestModifierConfig getRequestModifierConfig() { return getRuleConfig(); }
+
+ @Override public boolean couldModify(FilterHttpRequest request) {
+ final BubbleBlockConfig config = getRuleConfig();
+ return (config.inPageBlocks() || config.showStats()) && isHtml(request.getContentType());
+ }
+
@Override public void init(JsonNode config,
JsonNode userConfig,
AppRule rule,
@@ -170,6 +173,7 @@ public class BubbleBlockRuleDriver extends TrafficAnalyticsRuleDriver {
final BubbleBlockConfig bubbleBlockConfig = getRuleConfig();
final BlockDecision decision = getPreprocessDecision(filter.getFqdn(), filter.getUri(), filter.getUserAgent(), filter.getReferer());
final BlockDecisionType decisionType = decision.getDecisionType();
+ final FilterMatchDecision subDecision;
switch (decisionType) {
case block:
if (log.isInfoEnabled()) log.info(prefix+"decision is BLOCK");
@@ -178,19 +182,15 @@ public class BubbleBlockRuleDriver extends TrafficAnalyticsRuleDriver {
return FilterMatchDecision.abort_not_found; // block this request
case allow: default:
- if (filter.hasReferer()) {
- final FilterMatchDecision refererDecision = checkRefererDecision(filter, account, device, app, site, prefix);
- if (refererDecision != null) return refererDecision;
- }
+ subDecision = checkRefererAndShowStats(decisionType, filter, account, device, extraLog, app, site, prefix, bubbleBlockConfig);
+ if (subDecision != null) return subDecision;
if (log.isInfoEnabled()) log.info(prefix+"decision is ALLOW");
else if (extraLog) log.error(prefix+"decision is ALLOW");
return FilterMatchDecision.no_match;
case filter:
- if (filter.hasReferer()) {
- final FilterMatchDecision refererDecision = checkRefererDecision(filter, account, device, app, site, prefix);
- if (refererDecision != null) return refererDecision;
- }
+ subDecision = checkRefererAndShowStats(decisionType, filter, account, device, extraLog, app, site, prefix, bubbleBlockConfig);
+ if (subDecision != null) return subDecision;
final List specs = decision.getSpecs();
if (empty(specs)) {
if (log.isWarnEnabled()) log.warn(prefix+"decision was 'filter' but no specs were found, returning no_match");
@@ -216,6 +216,23 @@ public class BubbleBlockRuleDriver extends TrafficAnalyticsRuleDriver {
}
}
+ public FilterMatchDecision checkRefererAndShowStats(BlockDecisionType decisionType, FilterMatchersRequest filter, Account account, Device device, boolean extraLog, String app, String site, String prefix, BubbleBlockConfig bubbleBlockConfig) {
+ if (filter.hasReferer()) {
+ final FilterMatchDecision refererDecision = checkRefererDecision(filter, account, device, app, site, prefix);
+ if (refererDecision != null && refererDecision.isAbort()) {
+ if (log.isInfoEnabled()) log.info(prefix+"decision was "+decisionType+" but refererDecision was "+refererDecision+", returning "+refererDecision);
+ else if (extraLog) log.error(prefix+"decision was "+decisionType+" but refererDecision was "+refererDecision+", returning "+refererDecision);
+ return refererDecision;
+ }
+ }
+ if (bubbleBlockConfig.showStats()) {
+ if (log.isInfoEnabled()) log.info(prefix+"decision was "+decisionType+" but showStats=true, returning match");
+ else if (extraLog) log.error(prefix+"decision was "+decisionType+" but showStats=true, returning match");
+ return FilterMatchDecision.match;
+ }
+ return null;
+ }
+
public FilterMatchDecision checkRefererDecision(FilterMatchersRequest filter, Account account, Device device, String app, String site, String prefix) {
prefix = prefix+" (checkRefererDecision): ";
final URI refererURI = URIUtil.toUriOrNull(filter.getReferer());
@@ -268,10 +285,13 @@ public class BubbleBlockRuleDriver extends TrafficAnalyticsRuleDriver {
return false;
}
+ public static final String FILTER_CTX_DECISION = "decision";
+
@Override public InputStream doFilterResponse(FilterHttpRequest filterRequest, InputStream in) {
final FilterMatchersRequest request = filterRequest.getMatchersResponse().getRequest();
final String prefix = "doFilterResponse("+filterRequest.getId()+"): ";
+ final BubbleBlockConfig bubbleBlockConfig = getRuleConfig();
// todo: add support for stream blockers: we may allow the request but wrap the returned InputStream
// if the wrapper detects it should be blocked, then the connection cut short
@@ -281,6 +301,7 @@ public class BubbleBlockRuleDriver extends TrafficAnalyticsRuleDriver {
// Now that we know the content type, re-check the BlockList
final String contentType = filterRequest.getContentType();
final BlockDecision decision = getBlockList().getDecision(request.getFqdn(), request.getUri(), contentType, request.getReferer(), true);
+ final Map filterCtx = new SingletonMap<>(FILTER_CTX_DECISION, decision);
if (log.isDebugEnabled()) log.debug(prefix+"preprocess decision was "+decision+", but now we know contentType="+contentType);
switch (decision.getDecisionType()) {
case block:
@@ -312,33 +333,39 @@ public class BubbleBlockRuleDriver extends TrafficAnalyticsRuleDriver {
return in;
}
- final String replacement = "";
- final RegexReplacementFilter filter = new RegexReplacementFilter("", replacement);
- final RegexFilterReader reader = new RegexFilterReader(new InputStreamReader(in, UTF8cs), filter).setMaxMatches(1);
- if (log.isDebugEnabled()) {
- log.debug(prefix+"filtering response for "+request.getUrl()+" - replacement.length = "+replacement.length());
- } else if (log.isInfoEnabled()) {
- log.info(prefix+"SEND: filtering response for "+request.getUrl());
+ if (!bubbleBlockConfig.inPageBlocks() && !bubbleBlockConfig.showStats()) {
+ if (log.isInfoEnabled()) log.info(prefix + "SEND: both inPageBlocks and showStats are false, returning as-is");
+ return in;
+ }
+ if (bubbleBlockConfig.inPageBlocks() && bubbleBlockConfig.showStats()) {
+ return filterInsertJs(in, filterRequest, filterCtx, BUBBLE_JS_BOTH_TEMPLATE, null, null);
}
- return new ReaderInputStream(reader, UTF8cs);
+ if (bubbleBlockConfig.inPageBlocks()) {
+ return filterInsertJs(in, filterRequest, filterCtx, BUBBLE_JS_TEMPLATE, null, null);
+ }
+ log.warn(prefix+"doFilterResponse: inserting JS for stats...");
+ return filterInsertJs(in, filterRequest, filterCtx, BUBBLE_JS_STATS_TEMPLATE, null, null);
}
public static final Class BB = BubbleBlockRuleDriver.class;
public static final String BUBBLE_JS_TEMPLATE = stream2string(getPackagePath(BB)+"/"+ BB.getSimpleName()+".js.hbs");
+ public static final String BUBBLE_JS_STATS_TEMPLATE = stream2string(getPackagePath(BB)+"/"+ BB.getSimpleName()+"_stats.js.hbs");
+ public static final String BUBBLE_JS_BOTH_TEMPLATE = BUBBLE_JS_TEMPLATE + "\n\n" + BUBBLE_JS_STATS_TEMPLATE;
+
private static final String CTX_BUBBLE_SELECTORS = "BUBBLE_SELECTORS_JSON";
private static final String CTX_BUBBLE_BLACKLIST = "BUBBLE_BLACKLIST_JSON";
private static final String CTX_BUBBLE_WHITELIST = "BUBBLE_WHITELIST_JSON";
- private String getBubbleJs(String requestId, BlockDecision decision) {
- final Map ctx = new HashMap<>();
- ctx.put(CTX_JS_PREFIX, AppRuleDriver.getJsPrefix(requestId));
- ctx.put(CTX_BUBBLE_REQUEST_ID, requestId);
- ctx.put(CTX_BUBBLE_HOME, configuration.getPublicUriBase());
- ctx.put(CTX_BUBBLE_DATA_ID, getDataId(requestId));
- ctx.put(CTX_BUBBLE_SELECTORS, json(decision.getSelectors(), COMPACT_MAPPER));
- ctx.put(CTX_BUBBLE_WHITELIST, json(getBlockList().getWhitelistDomains(), COMPACT_MAPPER));
- ctx.put(CTX_BUBBLE_BLACKLIST, json(getBlockList().getBlacklistDomains(), COMPACT_MAPPER));
- return HandlebarsUtil.apply(getHandlebars(), BUBBLE_JS_TEMPLATE, ctx);
+ @Override protected Map getBubbleJsContext(String requestId, Map filterCtx) {
+ final Map ctx = super.getBubbleJsContext(requestId, filterCtx);
+ final BubbleBlockConfig bubbleBlockConfig = getRuleConfig();
+ if (bubbleBlockConfig.inPageBlocks()) {
+ final BlockDecision decision = (BlockDecision) filterCtx.get(FILTER_CTX_DECISION);
+ ctx.put(CTX_BUBBLE_SELECTORS, json(decision.getSelectors(), COMPACT_MAPPER));
+ ctx.put(CTX_BUBBLE_WHITELIST, json(getBlockList().getWhitelistDomains(), COMPACT_MAPPER));
+ ctx.put(CTX_BUBBLE_BLACKLIST, json(getBlockList().getBlacklistDomains(), COMPACT_MAPPER));
+ }
+ return ctx;
}
}
diff --git a/bubble-server/src/main/java/bubble/rule/social/block/JsUserBlockerConfig.java b/bubble-server/src/main/java/bubble/rule/social/block/JsUserBlockerConfig.java
index a2212604..b0017db6 100644
--- a/bubble-server/src/main/java/bubble/rule/social/block/JsUserBlockerConfig.java
+++ b/bubble-server/src/main/java/bubble/rule/social/block/JsUserBlockerConfig.java
@@ -4,26 +4,6 @@
*/
package bubble.rule.social.block;
-import bubble.rule.BubbleRegexReplacement;
-import lombok.Getter;
-import lombok.Setter;
+import bubble.rule.RequestModifierConfig;
-import static org.cobbzilla.util.daemon.ZillaRuntime.empty;
-
-public class JsUserBlockerConfig {
-
- @Getter @Setter private String siteJsTemplate;
-
- @Getter @Setter private String insertionRegex;
- public boolean hasInsertionRegex () { return !empty(insertionRegex); }
-
- @Getter @Setter private String scriptOpen;
- public boolean hasScriptOpen () { return !empty(scriptOpen); }
-
- @Getter @Setter private String scriptClose;
- public boolean hasScriptClose () { return !empty(scriptClose); }
-
- @Getter @Setter private BubbleRegexReplacement[] additionalRegexReplacements;
- public boolean hasAdditionalRegexReplacements () { return !empty(additionalRegexReplacements); }
-
-}
+public class JsUserBlockerConfig extends RequestModifierConfig {}
diff --git a/bubble-server/src/main/java/bubble/rule/social/block/JsUserBlockerRuleDriver.java b/bubble-server/src/main/java/bubble/rule/social/block/JsUserBlockerRuleDriver.java
index e2bab483..0a5f9bd1 100644
--- a/bubble-server/src/main/java/bubble/rule/social/block/JsUserBlockerRuleDriver.java
+++ b/bubble-server/src/main/java/bubble/rule/social/block/JsUserBlockerRuleDriver.java
@@ -4,120 +4,40 @@
*/
package bubble.rule.social.block;
-import bubble.model.app.AppMatcher;
import bubble.resources.stream.FilterHttpRequest;
import bubble.rule.AbstractAppRuleDriver;
-import bubble.rule.AppRuleDriver;
-import bubble.rule.BubbleRegexReplacement;
+import bubble.rule.RequestModifierConfig;
+import bubble.rule.RequestModifierRule;
+import bubble.rule.bblock.BubbleBlockConfig;
import lombok.Getter;
-import org.apache.commons.io.input.ReaderInputStream;
-import org.cobbzilla.util.collection.ExpirationMap;
-import org.cobbzilla.util.handlebars.HandlebarsUtil;
-import org.cobbzilla.util.io.FileUtil;
-import org.cobbzilla.util.io.regex.RegexFilterReader;
-import org.cobbzilla.util.io.regex.RegexReplacementFilter;
+import lombok.extern.slf4j.Slf4j;
-import java.io.File;
import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.util.HashMap;
-import java.util.Map;
-import static bubble.ApiConstants.HOME_DIR;
import static org.cobbzilla.util.http.HttpContentTypes.isHtml;
import static org.cobbzilla.util.io.StreamUtil.stream2string;
-import static org.cobbzilla.util.io.regex.RegexReplacementFilter.DEFAULT_PREFIX_REPLACEMENT_WITH_MATCH;
import static org.cobbzilla.util.json.JsonUtil.json;
-import static org.cobbzilla.util.string.StringUtil.UTF8cs;
import static org.cobbzilla.util.string.StringUtil.getPackagePath;
-public class JsUserBlockerRuleDriver extends AbstractAppRuleDriver {
+@Slf4j
+public class JsUserBlockerRuleDriver extends AbstractAppRuleDriver implements RequestModifierRule {
public static final Class JSB = JsUserBlockerRuleDriver.class;
public static final String BUBBLE_JS_TEMPLATE = stream2string(getPackagePath(JSB)+"/"+ JSB.getSimpleName()+".js.hbs");
public static final String CTX_APPLY_BLOCKS_JS = "APPLY_BLOCKS_JS";
- public static final String DEFAULT_INSERTION_REGEX = "<\\s*head[^>]*>";
- public static final String DEFAULT_SCRIPT_OPEN = "";
-
@Override public boolean couldModify(FilterHttpRequest request) { return true; }
- @Getter(lazy=true) private final JsUserBlockerConfig userBlockerConfig = json(config, JsUserBlockerConfig.class);
-
- @Override public InputStream doFilterResponse(FilterHttpRequest filterRequest, InputStream in) {
- if (!isHtml(filterRequest.getContentType())) return in;
- final String replacement = DEFAULT_PREFIX_REPLACEMENT_WITH_MATCH
- + getScriptOpen(filterRequest)
- + getBubbleJs(filterRequest.getId())
- + getScriptClose();
-
- final RegexReplacementFilter filter = new RegexReplacementFilter(getInsertionRegex(), replacement);
- RegexFilterReader reader = new RegexFilterReader(new InputStreamReader(in), filter).setMaxMatches(1);
- if (getUserBlockerConfig().hasAdditionalRegexReplacements()) {
- for (BubbleRegexReplacement re : getUserBlockerConfig().getAdditionalRegexReplacements()) {
- final RegexReplacementFilter f = new RegexReplacementFilter(re.getInsertionRegex(), re.getReplacement());
- reader = new RegexFilterReader(reader, f);
- }
- }
-
- return new ReaderInputStream(reader, UTF8cs);
- }
-
- @Getter(lazy=true) private final String insertionRegex = getUserBlockerConfig().hasInsertionRegex()
- ? getUserBlockerConfig().getInsertionRegex()
- : DEFAULT_INSERTION_REGEX;
-
- public String getScriptOpen(FilterHttpRequest filterRequest) {
- if (filterRequest.hasScriptNonce()) {
- // log.info("getScriptOpen: using nonce="+filterRequest.getScriptNonce());
- return getUserBlockerConfig().hasScriptOpen()
- ? getUserBlockerConfig().getScriptOpen().replace(NONCE_VAR, filterRequest.getScriptNonce())
- : DEFAULT_SCRIPT_NONCE_OPEN.replace(NONCE_VAR, filterRequest.getScriptNonce());
- } else {
- // log.info("getScriptOpen: no nonce");
- return getUserBlockerConfig().hasScriptOpen()
- ? getUserBlockerConfig().getScriptOpen()
- : DEFAULT_SCRIPT_OPEN;
- }
- }
+ @Override public Class getConfigClass() { return (Class) JsUserBlockerConfig.class; }
- @Getter(lazy=true) private final String scriptClose = getUserBlockerConfig().hasScriptClose()
- ? getUserBlockerConfig().getScriptClose()
- : DEFAULT_SCRIPT_CLOSE;
+ @Override public RequestModifierConfig getRequestModifierConfig() { return getRuleConfig(); }
- @Getter(lazy=true) private final String _siteJsTemplate = stream2string(getUserBlockerConfig().getSiteJsTemplate());
+ @Getter(lazy=true) private final String defaultSiteJsTemplate = stream2string(getRequestModifierConfig().getSiteJsTemplate());
- public String getSiteJsTemplate () {
- if (configuration.getEnvironment().containsKey("DEBUG_JS_SITE_TEMPLATES")) {
- final File jsTemplateFile = new File(HOME_DIR + "/siteJsTemplates/" + getUserBlockerConfig().getSiteJsTemplate());
- if (jsTemplateFile.exists()) {
- return FileUtil.toStringOrDie(jsTemplateFile);
- }
- }
- return get_siteJsTemplate();
- }
-
- private String getBubbleJs(String requestId) {
- final Map ctx = new HashMap<>();
- ctx.put(CTX_JS_PREFIX, AppRuleDriver.getJsPrefix(requestId));
- ctx.put(CTX_BUBBLE_REQUEST_ID, requestId);
- ctx.put(CTX_BUBBLE_HOME, configuration.getPublicUriBase());
- ctx.put(CTX_SITE, getSiteName(matcher));
- ctx.put(CTX_BUBBLE_DATA_ID, getDataId(requestId));
-
- final String siteJs = HandlebarsUtil.apply(getHandlebars(), getSiteJsTemplate(), ctx);
- ctx.put(CTX_APPLY_BLOCKS_JS, siteJs);
-
- return HandlebarsUtil.apply(getHandlebars(), BUBBLE_JS_TEMPLATE, ctx);
- }
-
- private ExpirationMap siteNameCache = new ExpirationMap<>();
- private String getSiteName(AppMatcher matcher) {
- return siteNameCache.computeIfAbsent(matcher.getSite(), k -> appSiteDAO.findByAccountAndId(matcher.getAccount(), matcher.getSite()).getName());
+ @Override public InputStream doFilterResponse(FilterHttpRequest filterRequest, InputStream in) {
+ if (!isHtml(filterRequest.getContentType())) return in;
+ log.warn("doFilterResponse: inserting JS, getRequestModifierConfig()="+json(getRequestModifierConfig()));
+ return filterInsertJs(in, filterRequest, null, BUBBLE_JS_TEMPLATE, getDefaultSiteJsTemplate(), CTX_APPLY_BLOCKS_JS);
}
-
}
diff --git a/bubble-server/src/main/resources/bubble/rule/bblock/BubbleBlockRuleDriver_stats.js.hbs b/bubble-server/src/main/resources/bubble/rule/bblock/BubbleBlockRuleDriver_stats.js.hbs
new file mode 100644
index 00000000..c78498fc
--- /dev/null
+++ b/bubble-server/src/main/resources/bubble/rule/bblock/BubbleBlockRuleDriver_stats.js.hbs
@@ -0,0 +1,3 @@
+//
+// block stats js goes here
+//
diff --git a/bubble-server/src/main/resources/logback.xml b/bubble-server/src/main/resources/logback.xml
index 95187ddd..78389fcb 100644
--- a/bubble-server/src/main/resources/logback.xml
+++ b/bubble-server/src/main/resources/logback.xml
@@ -53,8 +53,8 @@
+
-
diff --git a/bubble-server/src/main/resources/models/apps/bubble_block/bubbleApp_bubbleBlock.json b/bubble-server/src/main/resources/models/apps/bubble_block/bubbleApp_bubbleBlock.json
index 09cb6d6d..5e52b42b 100644
--- a/bubble-server/src/main/resources/models/apps/bubble_block/bubbleApp_bubbleBlock.json
+++ b/bubble-server/src/main/resources/models/apps/bubble_block/bubbleApp_bubbleBlock.json
@@ -126,6 +126,7 @@
"driver": "BubbleBlockRuleDriver",
"priority": -1000,
"config": {
+ "showStats": true,
"blockLists": [
{
"name": "EasyList",
diff --git a/bubble-server/src/main/resources/models/apps/user_block/hn/bubbleApp_userBlock_hn_matchers.json b/bubble-server/src/main/resources/models/apps/user_block/hn/bubbleApp_userBlock_hn_matchers.json
index 869f1b58..91ba2cf5 100644
--- a/bubble-server/src/main/resources/models/apps/user_block/hn/bubbleApp_userBlock_hn_matchers.json
+++ b/bubble-server/src/main/resources/models/apps/user_block/hn/bubbleApp_userBlock_hn_matchers.json
@@ -6,6 +6,7 @@
"site": "HackerNews",
"template": true,
"requestCheck": true,
+ "requestModifier": true,
"fqdn": "news.ycombinator.com",
"urlRegex": "/item\\?id=\\d+",
"rule": "hn_user_blocker"
@@ -14,6 +15,7 @@
"site": "HackerNews",
"template": true,
"requestCheck": true,
+ "requestModifier": true,
"fqdn": "news.ycombinator.com",
"urlRegex": "/threads\\?id=\\w+",
"rule": "hn_user_blocker"