@@ -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()); | |||
} | |||
} |
@@ -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"); } | |||
} |
@@ -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); } | |||
} |
@@ -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 (); | |||
} |
@@ -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); } | |||
@@ -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; | |||
} | |||
} |
@@ -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 {} |
@@ -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); | |||
} | |||
} |
@@ -0,0 +1,3 @@ | |||
// | |||
// block stats js goes here | |||
// |
@@ -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" />--> | |||
@@ -126,6 +126,7 @@ | |||
"driver": "BubbleBlockRuleDriver", | |||
"priority": -1000, | |||
"config": { | |||
"showStats": true, | |||
"blockLists": [ | |||
{ | |||
"name": "EasyList", | |||
@@ -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" | |||