Browse Source

WIP. refactor js insertion. add block stats stub json.

pull/43/head
Jonathan Cobb 4 years ago
parent
commit
1883527d39
12 changed files with 241 additions and 153 deletions
  1. +111
    -0
      bubble-server/src/main/java/bubble/rule/AbstractAppRuleDriver.java
  2. +3
    -1
      bubble-server/src/main/java/bubble/rule/FilterMatchDecision.java
  3. +26
    -0
      bubble-server/src/main/java/bubble/rule/RequestModifierConfig.java
  4. +11
    -0
      bubble-server/src/main/java/bubble/rule/RequestModifierRule.java
  5. +7
    -2
      bubble-server/src/main/java/bubble/rule/bblock/BubbleBlockConfig.java
  6. +61
    -34
      bubble-server/src/main/java/bubble/rule/bblock/BubbleBlockRuleDriver.java
  7. +2
    -22
      bubble-server/src/main/java/bubble/rule/social/block/JsUserBlockerConfig.java
  8. +13
    -93
      bubble-server/src/main/java/bubble/rule/social/block/JsUserBlockerRuleDriver.java
  9. +3
    -0
      bubble-server/src/main/resources/bubble/rule/bblock/BubbleBlockRuleDriver_stats.js.hbs
  10. +1
    -1
      bubble-server/src/main/resources/logback.xml
  11. +1
    -0
      bubble-server/src/main/resources/models/apps/bubble_block/bubbleApp_bubbleBlock.json
  12. +2
    -0
      bubble-server/src/main/resources/models/apps/user_block/hn/bubbleApp_userBlock_hn_matchers.json

+ 111
- 0
bubble-server/src/main/java/bubble/rule/AbstractAppRuleDriver.java View File

@@ -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 = "<meta charset=\"UTF-8\"><script>";
public static final String NONCE_VAR = "{{nonce}}";
public static final String DEFAULT_SCRIPT_NONCE_OPEN = "<meta charset=\"UTF-8\"><script nonce=\""+NONCE_VAR+"\">";
public static final String DEFAULT_SCRIPT_CLOSE = "</script>";

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<String, Object> 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<String, Object> filterCtx,
String bubbleJsTemplate,
String defaultSiteTemplate,
String siteJsInsertionVar) {
final Map<String, Object> 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<String, Object> getBubbleJsContext(String requestId, Map<String, Object> filterCtx) {
final Map<String, Object> 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<String, String> siteNameCache = new ExpirationMap<>();
protected String getSiteName(AppMatcher matcher) {
return siteNameCache.computeIfAbsent(matcher.getSite(), k -> appSiteDAO.findByAccountAndId(matcher.getAccount(), matcher.getSite()).getName());
}

}

+ 3
- 1
bubble-server/src/main/java/bubble/rule/FilterMatchDecision.java View File

@@ -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"); }

}

+ 26
- 0
bubble-server/src/main/java/bubble/rule/RequestModifierConfig.java View File

@@ -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); }

}

+ 11
- 0
bubble-server/src/main/java/bubble/rule/RequestModifierRule.java View File

@@ -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 ();

}

+ 7
- 2
bubble-server/src/main/java/bubble/rule/bblock/BubbleBlockConfig.java View File

@@ -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); }


+ 61
- 34
bubble-server/src/main/java/bubble/rule/bblock/BubbleBlockRuleDriver.java View File

@@ -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> blockList = new AtomicReference<>(new BlockList());
private BlockList getBlockList() { return blockList.get(); }
@@ -62,6 +58,13 @@ public class BubbleBlockRuleDriver extends TrafficAnalyticsRuleDriver {

@Override public <C> Class<C> getConfigClass() { return (Class<C>) 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<BlockSpec> 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<String, Object> 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 = "<head><script>" + getBubbleJs(filterRequest.getId(), decision) + "</script>";
final RegexReplacementFilter filter = new RegexReplacementFilter("<head>", 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<BubbleBlockRuleDriver> 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<String, Object> 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<String, Object> getBubbleJsContext(String requestId, Map<String, Object> filterCtx) {
final Map<String, Object> 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;
}

}

+ 2
- 22
bubble-server/src/main/java/bubble/rule/social/block/JsUserBlockerConfig.java View File

@@ -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 {}

+ 13
- 93
bubble-server/src/main/java/bubble/rule/social/block/JsUserBlockerRuleDriver.java View File

@@ -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<JsUserBlockerRuleDriver> 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 = "<meta charset=\"UTF-8\"><script>";
public static final String NONCE_VAR = "{{nonce}}";
public static final String DEFAULT_SCRIPT_NONCE_OPEN = "<meta charset=\"UTF-8\"><script nonce=\""+NONCE_VAR+"\">";
public static final String DEFAULT_SCRIPT_CLOSE = "</script>";

@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 <C> Class<C> getConfigClass() { return (Class<C>) 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<String, Object> 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<String, String> 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);
}

}

+ 3
- 0
bubble-server/src/main/resources/bubble/rule/bblock/BubbleBlockRuleDriver_stats.js.hbs View File

@@ -0,0 +1,3 @@
//
// block stats js goes here
//

+ 1
- 1
bubble-server/src/main/resources/logback.xml View File

@@ -53,8 +53,8 @@
<!-- <logger name="bubble.service.stream.StandardRuleEngineService" level="DEBUG" />-->
<logger name="bubble.service.stream.ActiveStreamState" level="WARN" />
<logger name="bubble.resources.stream" level="WARN" />
<!-- <logger name="bubble.resources.stream.FilterHttpResource" level="DEBUG" />-->
<logger name="bubble.resources.stream.FilterHttpResource" level="WARN" />
<!-- <logger name="bubble.resources.stream.FilterHttpResource" level="INFO" />-->
<logger name="bubble.service.stream" level="INFO" />
<!-- <logger name="bubble.service.account.StandardAccountMessageService" level="DEBUG" />-->
<!-- <logger name="bubble.dao.account.message.AccountMessageDAO" level="DEBUG" />-->


+ 1
- 0
bubble-server/src/main/resources/models/apps/bubble_block/bubbleApp_bubbleBlock.json View File

@@ -126,6 +126,7 @@
"driver": "BubbleBlockRuleDriver",
"priority": -1000,
"config": {
"showStats": true,
"blockLists": [
{
"name": "EasyList",


+ 2
- 0
bubble-server/src/main/resources/models/apps/user_block/hn/bubbleApp_userBlock_hn_matchers.json View File

@@ -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"


Loading…
Cancel
Save