@@ -0,0 +1,33 @@ | |||
/** | |||
* Copyright (c) 2020 Bubble, Inc. All rights reserved. | |||
* For personal (non-commercial) use, see license: https://bubblev.com/bubble-license/ | |||
*/ | |||
package bubble.app.passthru; | |||
import bubble.model.account.Account; | |||
import bubble.model.app.BubbleApp; | |||
import bubble.model.app.config.AppConfigDriver; | |||
import com.fasterxml.jackson.databind.JsonNode; | |||
import lombok.extern.slf4j.Slf4j; | |||
import java.util.Map; | |||
@Slf4j | |||
public class TlsPassthruAppConfigDriver implements AppConfigDriver { | |||
@Override public Object getView(Account account, BubbleApp app, String view, Map<String, String> params) { | |||
// todo | |||
return null; | |||
} | |||
@Override public Object takeAppAction(Account account, BubbleApp app, String view, String action, Map<String, String> params, JsonNode data) { | |||
// todo | |||
return null; | |||
} | |||
@Override public Object takeItemAction(Account account, BubbleApp app, String view, String action, String id, Map<String, String> params, JsonNode data) { | |||
// todo | |||
return null; | |||
} | |||
} |
@@ -0,0 +1,9 @@ | |||
/** | |||
* Copyright (c) 2020 Bubble, Inc. All rights reserved. | |||
* For personal (non-commercial) use, see license: https://bubblev.com/bubble-license/ | |||
*/ | |||
package bubble.app.passthru; | |||
import bubble.model.app.config.AppDataDriverBase; | |||
public class TlsPassthruAppDataDriver extends AppDataDriverBase {} |
@@ -20,7 +20,7 @@ public class BubbleAppDAO extends AccountOwnedTemplateDAO<BubbleApp> { | |||
@Autowired private AppDataDAO dataDAO; | |||
@Autowired private RuleEngineService ruleEngineService; | |||
@Override public Order getDefaultSortOrder() { return NAME_ASC; } | |||
@Override public Order getDefaultSortOrder() { return PRIORITY_ASC; } | |||
@Override public BubbleApp postUpdate(BubbleApp app, Object context) { | |||
ruleEngineService.flushCaches(); | |||
@@ -14,6 +14,7 @@ import lombok.NoArgsConstructor; | |||
import lombok.Setter; | |||
import lombok.experimental.Accessors; | |||
import org.cobbzilla.util.collection.ArrayUtil; | |||
import org.cobbzilla.util.collection.HasPriority; | |||
import org.cobbzilla.wizard.model.Identifiable; | |||
import org.cobbzilla.wizard.model.entityconfig.IdentifiableBaseParentEntity; | |||
import org.cobbzilla.wizard.model.entityconfig.annotations.*; | |||
@@ -26,6 +27,7 @@ import javax.persistence.Transient; | |||
import javax.validation.constraints.Size; | |||
import static bubble.ApiConstants.*; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.empty; | |||
import static org.cobbzilla.util.json.JsonUtil.json; | |||
import static org.cobbzilla.util.reflect.ReflectionUtil.copy; | |||
import static org.cobbzilla.wizard.model.crypto.EncryptedTypes.ENCRYPTED_STRING; | |||
@@ -47,9 +49,9 @@ import static org.cobbzilla.wizard.model.crypto.EncryptedTypes.ENC_PAD; | |||
@ECIndex(of={"account", "template", "enabled"}), | |||
@ECIndex(of={"template", "enabled"}) | |||
}) | |||
public class BubbleApp extends IdentifiableBaseParentEntity implements AccountTemplate { | |||
public class BubbleApp extends IdentifiableBaseParentEntity implements AccountTemplate, HasPriority { | |||
private static final String[] VALUE_FIELDS = {"url", "description", "template", "enabled", "dataConfig"}; | |||
private static final String[] VALUE_FIELDS = {"url", "description", "template", "enabled", "priority", "dataConfig"}; | |||
public BubbleApp(Account account, BubbleApp app) { | |||
copy(this, app); | |||
@@ -90,9 +92,11 @@ public class BubbleApp extends IdentifiableBaseParentEntity implements AccountTe | |||
public boolean hasDataConfig () { return getDataConfig() != null; } | |||
private AppDataConfig ensureDefaults(AppDataConfig adc) { | |||
for (AppDataField field : adc.getFields()) { | |||
if (!adc.hasConfigField(field)) { | |||
adc.setConfigFields(ArrayUtil.append(adc.getConfigFields(), field)); | |||
if (!empty(adc.getFields())) { | |||
for (AppDataField field : adc.getFields()) { | |||
if (!adc.hasConfigField(field)) { | |||
adc.setConfigFields(ArrayUtil.append(adc.getConfigFields(), field)); | |||
} | |||
} | |||
} | |||
return adc; | |||
@@ -114,7 +118,10 @@ public class BubbleApp extends IdentifiableBaseParentEntity implements AccountTe | |||
@ECIndex @Column(nullable=false) | |||
@Getter @Setter private Boolean enabled = true; | |||
@ECSearchable @ECField(index=90) | |||
@ECSearchable @ECField(index=90) @Column(nullable=false) | |||
@ECIndex @Getter @Setter private Integer priority; | |||
@ECSearchable @ECField(index=100) | |||
@ECIndex @Getter @Setter private Boolean needsUpdate = false; | |||
} |
@@ -93,14 +93,19 @@ public class MessagesResource { | |||
private Map<String, String> loadMessages(Account caller, String locale, String group, MessageResourceFormat format) throws IOException { | |||
final boolean isAppsGroup = group.equalsIgnoreCase(APPS_MESSAGE_GROUP); | |||
if (isAppsGroup && caller == null) return Collections.emptyMap(); | |||
if (isAppsGroup && caller == null) { | |||
if (log.isDebugEnabled()) log.debug("loadMessages: returning empty app messages for caller=null"); | |||
return Collections.emptyMap(); | |||
} | |||
locale = normalizeLocale(locale); | |||
final String cacheKey = (isAppsGroup ? caller.getUuid()+"/" : "") + locale + "/" + group + "/" + format; | |||
if (!messageCache.containsKey(cacheKey)) { | |||
final Properties props; | |||
if (isAppsGroup) { | |||
if (log.isDebugEnabled()) log.debug("loadMessages: loading app messages for caller="+caller.getName()+", locale="+locale); | |||
props = appMessageService.loadAppMessages(caller, locale); | |||
if (log.isDebugEnabled()) log.debug("loadMessages: loaded app messages for caller="+caller.getName()+", locale="+locale+", props.size="+props.size()); | |||
} else { | |||
props = new Properties(); | |||
props.load(new BufferedReader(new InputStreamReader(loadResourceAsStream(MESSAGE_RESOURCE_BASE + locale + MESSAGE_RESOURCE_PATH + group + "/" + RESOURCE_MESSAGES_PROPS), UTF8cs))); | |||
@@ -206,6 +206,7 @@ public class FilterHttpResource { | |||
case abort_ok: return FilterMatchersResponse.ABORT_OK; | |||
case abort_not_found: return FilterMatchersResponse.ABORT_NOT_FOUND; | |||
case no_match: break; | |||
case pass_thru: return FilterMatchersResponse.PASS_THRU; | |||
case match: retainMatchers.put(matcher.getUuid(), matcher); break; | |||
} | |||
} else { | |||
@@ -336,12 +337,12 @@ public class FilterHttpResource { | |||
final FilterMatchDecision decision = matchersResponse.getDecision(); | |||
if (decision != FilterMatchDecision.match) { | |||
switch (decision) { | |||
case no_match: | |||
if (log.isWarnEnabled()) log.warn(prefix + "FilterMatchersResponse decision was not match: "+ decision +", returning passthru"); | |||
case no_match: case pass_thru: | |||
if (log.isWarnEnabled()) log.warn(prefix + "FilterMatchersResponse decision was no_match/pass_thru (should not have received this): "+ decision +", returning passthru"); | |||
return passthru(request); | |||
case abort_not_found: | |||
case abort_ok: | |||
if (log.isWarnEnabled()) log.warn(prefix + "FilterMatchersResponse decision was not match: "+ decision +", returning "+matchersResponse.httpStatus()); | |||
if (log.isWarnEnabled()) log.warn(prefix + "FilterMatchersResponse decision was abort: "+ decision +", returning "+matchersResponse.httpStatus()); | |||
return status(matchersResponse.httpStatus()); | |||
default: | |||
if (log.isWarnEnabled()) log.warn(prefix + "FilterMatchersResponse decision was unknown: "+ decision +", returning passthru"); | |||
@@ -23,6 +23,7 @@ public class FilterMatchersResponse { | |||
public static final FilterMatchersResponse NO_MATCHERS = new FilterMatchersResponse().setDecision(FilterMatchDecision.no_match); | |||
public static final FilterMatchersResponse ABORT_OK = new FilterMatchersResponse().setDecision(FilterMatchDecision.abort_ok); | |||
public static final FilterMatchersResponse ABORT_NOT_FOUND = new FilterMatchersResponse().setDecision(FilterMatchDecision.abort_not_found); | |||
public static final FilterMatchersResponse PASS_THRU = new FilterMatchersResponse().setDecision(FilterMatchDecision.pass_thru); | |||
@Getter @Setter private FilterMatchersRequest request; | |||
public boolean hasRequest () { return request != null; } | |||
@@ -15,10 +15,11 @@ import static org.cobbzilla.util.http.HttpStatusCodes.OK; | |||
@AllArgsConstructor | |||
public enum FilterMatchDecision { | |||
no_match (OK), // associated matcher should not be included in request processing | |||
match (OK), // associated 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 | |||
no_match (OK), // associated matcher should not be included in request processing | |||
match (OK), // associated 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 | |||
@JsonCreator public static FilterMatchDecision fromString (String v) { return enumFromString(FilterMatchDecision.class, v); } | |||
@@ -0,0 +1,31 @@ | |||
/** | |||
* Copyright (c) 2020 Bubble, Inc. All rights reserved. | |||
* For personal (non-commercial) use, see license: https://bubblev.com/bubble-license/ | |||
*/ | |||
package bubble.rule.passthru; | |||
import com.fasterxml.jackson.annotation.JsonIgnore; | |||
import lombok.Getter; | |||
import lombok.Setter; | |||
import java.util.Arrays; | |||
import java.util.HashSet; | |||
import java.util.Set; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.empty; | |||
public class TlsPassthruConfig { | |||
@Getter @Setter private String[] passthruFqdn; | |||
@JsonIgnore @Getter(lazy=true) private final Set<String> passthruSet = initPassthruSet(); | |||
private Set<String> initPassthruSet() { | |||
final Set<String> set = new HashSet<>(); | |||
if (!empty(passthruFqdn)) set.addAll(Arrays.asList(passthruFqdn)); | |||
return set; | |||
} | |||
public boolean isPassthru(String fqdn) { return getPassthruSet().contains(fqdn); } | |||
} |
@@ -0,0 +1,47 @@ | |||
/** | |||
* Copyright (c) 2020 Bubble, Inc. All rights reserved. | |||
* For personal (non-commercial) use, see license: https://bubblev.com/bubble-license/ | |||
*/ | |||
package bubble.rule.passthru; | |||
import bubble.model.account.Account; | |||
import bubble.model.app.AppMatcher; | |||
import bubble.model.app.AppRule; | |||
import bubble.model.device.Device; | |||
import bubble.resources.stream.FilterMatchersRequest; | |||
import bubble.rule.AbstractAppRuleDriver; | |||
import bubble.rule.FilterMatchDecision; | |||
import bubble.service.stream.AppRuleHarness; | |||
import com.fasterxml.jackson.databind.JsonNode; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.glassfish.grizzly.http.server.Request; | |||
import org.glassfish.jersey.server.ContainerRequest; | |||
import static org.cobbzilla.util.json.JsonUtil.json; | |||
@Slf4j | |||
public class TlsPassthruRuleDriver extends AbstractAppRuleDriver { | |||
private TlsPassthruConfig passthruConfig; | |||
@Override public void init(JsonNode config, JsonNode userConfig, AppRule rule, AppMatcher matcher, Account account, Device device) { | |||
super.init(config, userConfig, rule, matcher, account, device); | |||
passthruConfig = json(json(config), TlsPassthruConfig.class); | |||
} | |||
@Override public FilterMatchDecision preprocess(AppRuleHarness ruleHarness, | |||
FilterMatchersRequest filter, | |||
Account account, | |||
Device device, | |||
Request req, | |||
ContainerRequest request) { | |||
final String fqdn = filter.getFqdn(); | |||
if (passthruConfig.isPassthru(fqdn)) { | |||
if (log.isDebugEnabled()) log.debug("preprocess: returning pass_thru for fqdn="+fqdn); | |||
return FilterMatchDecision.pass_thru; | |||
} | |||
if (log.isDebugEnabled()) log.debug("preprocess: returning no_match for fqdn="+fqdn); | |||
return FilterMatchDecision.no_match; | |||
} | |||
} |
@@ -19,6 +19,7 @@ import org.springframework.stereotype.Service; | |||
import java.util.Map; | |||
import java.util.Properties; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.empty; | |||
import static org.cobbzilla.util.string.StringUtil.EMPTY; | |||
@Service @Slf4j | |||
@@ -125,8 +126,10 @@ public class AppMessageService { | |||
} | |||
// anything from data fields not yet defined, copy as config field name/desc | |||
for (AppDataField field : cfg.getFields()) { | |||
ensureFieldNameAndDescription(props, cfgKeyPrefix, field.getName()); | |||
if (!empty(cfg.getFields())) { | |||
for (AppDataField field : cfg.getFields()) { | |||
ensureFieldNameAndDescription(props, cfgKeyPrefix, field.getName()); | |||
} | |||
} | |||
} | |||
} | |||
@@ -55,6 +55,7 @@ | |||
<logger name="org.cobbzilla.util.io.regex.RegexFilterReader" level="WARN" /> | |||
<logger name="org.cobbzilla.util.io.multi" level="INFO" /> | |||
<logger name="bubble.rule.social.block" level="INFO" /> | |||
<logger name="bubble.rule.passthru" level="DEBUG" /> | |||
<!-- <logger name="bubble.service.cloud.StandardNetworkService" level="INFO" />--> | |||
<logger name="bubble.resources.notify" level="WARN" /> | |||
<logger name="bubble.client" level="WARN" /> | |||
@@ -4,6 +4,7 @@ | |||
"url": "https://bubblev.com/apps/analytics", | |||
"template": true, | |||
"enabled": true, | |||
"priority": 100, | |||
"dataConfig": { | |||
"dataDriver": "bubble.app.analytics.TrafficAnalyticsAppDataDriver", | |||
"presentation": "app", | |||
@@ -4,6 +4,7 @@ | |||
"url": "https://bubblev.com/apps/bblock", | |||
"template": true, | |||
"enabled": true, | |||
"priority": 200, | |||
"dataConfig": { | |||
"dataDriver": "bubble.app.bblock.BubbleBlockAppDataDriver", | |||
"presentation": "app", | |||
@@ -0,0 +1,75 @@ | |||
[{ | |||
"name": "TlsPassthru", | |||
"description": "Do not perform SSL interception for certificate-pinned domains", | |||
"url": "https://bubblev.com/apps/passthru", | |||
"template": true, | |||
"enabled": true, | |||
"priority": 1000000, | |||
"dataConfig": { | |||
"dataDriver": "bubble.app.passthru.TlsPassthruAppDataDriver", | |||
"presentation": "app", | |||
"configDriver": "bubble.app.passthru.TlsPassthruAppConfigDriver", | |||
"configFields": [ | |||
{"name": "name"}, | |||
{"name": "description", "control": "textarea"}, | |||
{"name": "url", "type": "http_url"}, | |||
{"name": "tags"}, | |||
{"name": "tagString"}, | |||
{"name": "enabled", "type": "flag", "mode": "readOnly"}, | |||
{"name": "rule"}, | |||
{"name": "ruleType", "mode": "readOnly"}, | |||
{"name": "testUrl", "type": "http_url"}, | |||
{"name": "testUserAgent", "required": false}, | |||
{"name": "testUrlPrimary", "type": "flag"}, | |||
{"name": "urlRegex", "required": false}, | |||
{"name": "userAgentRegex"} | |||
], | |||
"configViews": [{ | |||
"name": "managePassthru", | |||
"scope": "app", | |||
"root": "true", | |||
"fields": ["fqdn"], | |||
"actions": [ | |||
{"name": "removeFqdn", "index": 10}, | |||
{ | |||
"name": "addFqdn", "scope": "app", "index": 10, | |||
"params": ["fqdn"], | |||
"button": "addFqdn" | |||
} | |||
] | |||
}] | |||
}, | |||
"children": { | |||
"AppSite": [{ | |||
"name": "All_Sites", | |||
"url": "*", | |||
"description": "All websites", | |||
"template": true | |||
}], | |||
"AppRule": [{ | |||
"name": "passthru", | |||
"template": true, | |||
"driver": "TlsPassthruRuleDriver", | |||
"priority": -1000000, | |||
"config": { | |||
"passthruFqdn": [] | |||
} | |||
}], | |||
"AppMessage": [{ | |||
"locale": "en_US", | |||
"messages": [ | |||
{"name": "name", "value": "DirectConnect"}, | |||
{"name": "icon", "value": "classpath:models/apps/passthru/passthru-icon.svg"}, | |||
{"name": "summary", "value": "Network Bypass"}, | |||
{"name": "description", "value": "Do not perform SSL interception for certificate-pinned domains"}, | |||
{"name": "config.view.managePassthru", "value": "Manage Bypass Domains"}, | |||
{"name": "config.field.fqdn", "value": "Hostname"}, | |||
{"name": "config.field.fqdn.description", "value": "Bypass traffic interception for this hostname"}, | |||
{"name": "config.action.addFqdn", "value": "Add New"}, | |||
{"name": "config.button.addFqdn", "value": "Add"}, | |||
{"name": "config.action.removeFqdn", "value": "Remove"} | |||
] | |||
}] | |||
} | |||
}] |
@@ -0,0 +1,14 @@ | |||
[{ | |||
"name": "TlsPassthru", | |||
"children": { | |||
"AppMatcher": [{ | |||
"name": "TlsPassthruMatcher", | |||
"template": true, | |||
"site": "All_Sites", | |||
"fqdn": "*", | |||
"urlRegex": ".*", | |||
"rule": "passthru", | |||
"priority": -1000000 | |||
}] | |||
} | |||
}] |
@@ -0,0 +1,105 @@ | |||
<?xml version="1.0" encoding="UTF-8" standalone="no"?> | |||
<!-- Created with Inkscape (http://www.inkscape.org/) --> | |||
<svg | |||
xmlns:dc="http://purl.org/dc/elements/1.1/" | |||
xmlns:cc="http://creativecommons.org/ns#" | |||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" | |||
xmlns:svg="http://www.w3.org/2000/svg" | |||
xmlns="http://www.w3.org/2000/svg" | |||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" | |||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" | |||
version="1.1" | |||
width="80" | |||
height="80" | |||
id="svg10029" | |||
sodipodi:docname="Hazard light icon.svg" | |||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"> | |||
<metadata | |||
id="metadata9"> | |||
<rdf:RDF> | |||
<cc:Work | |||
rdf:about=""> | |||
<dc:format>image/svg+xml</dc:format> | |||
<dc:type | |||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> | |||
<dc:title></dc:title> | |||
</cc:Work> | |||
</rdf:RDF> | |||
</metadata> | |||
<sodipodi:namedview | |||
pagecolor="#ffffff" | |||
bordercolor="#666666" | |||
borderopacity="1" | |||
objecttolerance="10" | |||
gridtolerance="10" | |||
guidetolerance="10" | |||
inkscape:pageopacity="0" | |||
inkscape:pageshadow="2" | |||
inkscape:window-width="1680" | |||
inkscape:window-height="998" | |||
id="namedview7" | |||
showgrid="true" | |||
showguides="true" | |||
inkscape:guide-bbox="true" | |||
inkscape:snap-page="true" | |||
inkscape:zoom="2.085965" | |||
inkscape:cx="-18.750976" | |||
inkscape:cy="1.4877119" | |||
inkscape:window-x="-8" | |||
inkscape:window-y="-8" | |||
inkscape:window-maximized="1" | |||
inkscape:current-layer="g10556"> | |||
<sodipodi:guide | |||
position="-143,40" | |||
orientation="0,1" | |||
id="guide819" | |||
inkscape:locked="false" /> | |||
<inkscape:grid | |||
type="xygrid" | |||
id="grid823" /> | |||
<sodipodi:guide | |||
position="40,55" | |||
orientation="1,0" | |||
id="guide4529" | |||
inkscape:locked="false" /> | |||
</sodipodi:namedview> | |||
<defs | |||
id="defs10032" /> | |||
<g | |||
transform="translate(-19.885022,2.1230082)" | |||
id="g10556"> | |||
<path | |||
sodipodi:type="star" | |||
style="fill:none;fill-opacity:1;stroke:#ff0000;stroke-width:4.5;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none" | |||
id="path4531" | |||
sodipodi:sides="3" | |||
sodipodi:cx="59.885021" | |||
sodipodi:cy="47.876992" | |||
sodipodi:r1="40" | |||
sodipodi:r2="20" | |||
sodipodi:arg1="-1.5707963" | |||
sodipodi:arg2="-0.52359878" | |||
inkscape:flatsided="false" | |||
inkscape:rounded="0" | |||
inkscape:randomized="0" | |||
d="m 59.885022,7.8769913 17.320507,29.9999997 17.320508,30.000001 -34.641016,-10e-7 -34.641016,-10e-7 17.320508,-29.999999 z" | |||
inkscape:transform-center-y="-10" /> | |||
<path | |||
inkscape:transform-center-y="-6.25" | |||
d="m 59.885022,22.876992 10.825317,18.75 10.825317,18.75 -21.650635,0 -21.650635,-1e-6 10.825318,-18.749999 z" | |||
inkscape:randomized="0" | |||
inkscape:rounded="0" | |||
inkscape:flatsided="false" | |||
sodipodi:arg2="-0.52359878" | |||
sodipodi:arg1="-1.5707963" | |||
sodipodi:r2="12.5" | |||
sodipodi:r1="25" | |||
sodipodi:cy="47.876992" | |||
sodipodi:cx="59.885021" | |||
sodipodi:sides="3" | |||
id="path4533" | |||
style="fill:none;fill-opacity:1;stroke:#ff0000;stroke-width:4.5;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none" | |||
sodipodi:type="star" /> | |||
</g> | |||
</svg> |
@@ -3,6 +3,8 @@ | |||
"description": "ShadowBan User Blocker", | |||
"url": "https://bubblev.com/apps/UserBlocker", | |||
"template": true, | |||
"enabled": true, | |||
"priority": 300, | |||
"dataConfig": { | |||
"dataDriver": "bubble.app.social.block.UserBlockerAppDataDriver", | |||
"presentation": "site", | |||
@@ -1,34 +1,32 @@ | |||
[ | |||
{ | |||
"name": "TlsPassthruRuleDriver", | |||
"driverClass": "bubble.rule.passthru.TlsPassthruRuleDriver", | |||
"template": true, | |||
"userConfig": { "fields": [] } | |||
}, | |||
{ | |||
"name": "UserBlockerRuleDriver", | |||
"driverClass": "bubble.rule.social.block.UserBlockerRuleDriver", | |||
"template": true, | |||
"userConfig": { | |||
"fields": [] | |||
} | |||
"userConfig": { "fields": [] } | |||
}, | |||
{ | |||
"name": "JsUserBlockerRuleDriver", | |||
"driverClass": "bubble.rule.social.block.JsUserBlockerRuleDriver", | |||
"template": true, | |||
"userConfig": { | |||
"fields": [] | |||
} | |||
"userConfig": { "fields": [] } | |||
}, | |||
{ | |||
"name": "TrafficAnalyticsRuleDriver", | |||
"driverClass": "bubble.rule.analytics.TrafficAnalyticsRuleDriver", | |||
"template": true, | |||
"userConfig": { | |||
"fields": [] | |||
} | |||
"userConfig": { "fields": [] } | |||
}, | |||
{ | |||
"name": "BubbleBlockRuleDriver", | |||
"driverClass": "bubble.rule.bblock.BubbleBlockRuleDriver", | |||
"template": true, | |||
"userConfig": { | |||
"fields": [] | |||
} | |||
"userConfig": { "fields": [] } | |||
} | |||
] |
@@ -1 +1 @@ | |||
Subproject commit 725052160cf969e5b2f041b0f764710e433feb7e | |||
Subproject commit 7cd7e0e140da2d048ad3addfac4bceb72ab5e89a |