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"