diff --git a/bubble-server/src/main/java/bubble/app/passthru/TlsPassthruAppConfigDriver.java b/bubble-server/src/main/java/bubble/app/passthru/TlsPassthruAppConfigDriver.java new file mode 100644 index 00000000..2ea21e9b --- /dev/null +++ b/bubble-server/src/main/java/bubble/app/passthru/TlsPassthruAppConfigDriver.java @@ -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 params) { + // todo + return null; + } + + @Override public Object takeAppAction(Account account, BubbleApp app, String view, String action, Map params, JsonNode data) { + // todo + return null; + } + + @Override public Object takeItemAction(Account account, BubbleApp app, String view, String action, String id, Map params, JsonNode data) { + // todo + return null; + } + +} diff --git a/bubble-server/src/main/java/bubble/app/passthru/TlsPassthruAppDataDriver.java b/bubble-server/src/main/java/bubble/app/passthru/TlsPassthruAppDataDriver.java new file mode 100644 index 00000000..4d776d39 --- /dev/null +++ b/bubble-server/src/main/java/bubble/app/passthru/TlsPassthruAppDataDriver.java @@ -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 {} diff --git a/bubble-server/src/main/java/bubble/dao/app/BubbleAppDAO.java b/bubble-server/src/main/java/bubble/dao/app/BubbleAppDAO.java index f15c6e9b..f2c58045 100644 --- a/bubble-server/src/main/java/bubble/dao/app/BubbleAppDAO.java +++ b/bubble-server/src/main/java/bubble/dao/app/BubbleAppDAO.java @@ -20,7 +20,7 @@ public class BubbleAppDAO extends AccountOwnedTemplateDAO { @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(); diff --git a/bubble-server/src/main/java/bubble/model/app/BubbleApp.java b/bubble-server/src/main/java/bubble/model/app/BubbleApp.java index 706db1dc..263556a6 100644 --- a/bubble-server/src/main/java/bubble/model/app/BubbleApp.java +++ b/bubble-server/src/main/java/bubble/model/app/BubbleApp.java @@ -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; } diff --git a/bubble-server/src/main/java/bubble/resources/message/MessagesResource.java b/bubble-server/src/main/java/bubble/resources/message/MessagesResource.java index a25a0660..1913b9dc 100644 --- a/bubble-server/src/main/java/bubble/resources/message/MessagesResource.java +++ b/bubble-server/src/main/java/bubble/resources/message/MessagesResource.java @@ -93,14 +93,19 @@ public class MessagesResource { private Map 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))); diff --git a/bubble-server/src/main/java/bubble/resources/stream/FilterHttpResource.java b/bubble-server/src/main/java/bubble/resources/stream/FilterHttpResource.java index 41c63610..3ba59da4 100644 --- a/bubble-server/src/main/java/bubble/resources/stream/FilterHttpResource.java +++ b/bubble-server/src/main/java/bubble/resources/stream/FilterHttpResource.java @@ -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"); diff --git a/bubble-server/src/main/java/bubble/resources/stream/FilterMatchersResponse.java b/bubble-server/src/main/java/bubble/resources/stream/FilterMatchersResponse.java index d94237e9..99806ca5 100644 --- a/bubble-server/src/main/java/bubble/resources/stream/FilterMatchersResponse.java +++ b/bubble-server/src/main/java/bubble/resources/stream/FilterMatchersResponse.java @@ -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; } diff --git a/bubble-server/src/main/java/bubble/rule/FilterMatchDecision.java b/bubble-server/src/main/java/bubble/rule/FilterMatchDecision.java index 0cc369a4..5152b501 100644 --- a/bubble-server/src/main/java/bubble/rule/FilterMatchDecision.java +++ b/bubble-server/src/main/java/bubble/rule/FilterMatchDecision.java @@ -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); } diff --git a/bubble-server/src/main/java/bubble/rule/passthru/TlsPassthruConfig.java b/bubble-server/src/main/java/bubble/rule/passthru/TlsPassthruConfig.java new file mode 100644 index 00000000..559fb4e7 --- /dev/null +++ b/bubble-server/src/main/java/bubble/rule/passthru/TlsPassthruConfig.java @@ -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 passthruSet = initPassthruSet(); + + private Set initPassthruSet() { + final Set set = new HashSet<>(); + if (!empty(passthruFqdn)) set.addAll(Arrays.asList(passthruFqdn)); + return set; + } + + public boolean isPassthru(String fqdn) { return getPassthruSet().contains(fqdn); } + +} diff --git a/bubble-server/src/main/java/bubble/rule/passthru/TlsPassthruRuleDriver.java b/bubble-server/src/main/java/bubble/rule/passthru/TlsPassthruRuleDriver.java new file mode 100644 index 00000000..726b9da2 --- /dev/null +++ b/bubble-server/src/main/java/bubble/rule/passthru/TlsPassthruRuleDriver.java @@ -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; + } + +} diff --git a/bubble-server/src/main/java/bubble/service/message/AppMessageService.java b/bubble-server/src/main/java/bubble/service/message/AppMessageService.java index 239491ea..28e53139 100644 --- a/bubble-server/src/main/java/bubble/service/message/AppMessageService.java +++ b/bubble-server/src/main/java/bubble/service/message/AppMessageService.java @@ -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()); + } } } } diff --git a/bubble-server/src/main/resources/logback.xml b/bubble-server/src/main/resources/logback.xml index 4c792652..6fbbe7a1 100644 --- a/bubble-server/src/main/resources/logback.xml +++ b/bubble-server/src/main/resources/logback.xml @@ -55,6 +55,7 @@ + diff --git a/bubble-server/src/main/resources/models/apps/analytics/bubbleApp_analytics.json b/bubble-server/src/main/resources/models/apps/analytics/bubbleApp_analytics.json index 1665ed39..6d1fea63 100644 --- a/bubble-server/src/main/resources/models/apps/analytics/bubbleApp_analytics.json +++ b/bubble-server/src/main/resources/models/apps/analytics/bubbleApp_analytics.json @@ -4,6 +4,7 @@ "url": "https://bubblev.com/apps/analytics", "template": true, "enabled": true, + "priority": 100, "dataConfig": { "dataDriver": "bubble.app.analytics.TrafficAnalyticsAppDataDriver", "presentation": "app", 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 827a1b09..35e3562a 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 @@ -4,6 +4,7 @@ "url": "https://bubblev.com/apps/bblock", "template": true, "enabled": true, + "priority": 200, "dataConfig": { "dataDriver": "bubble.app.bblock.BubbleBlockAppDataDriver", "presentation": "app", diff --git a/bubble-server/src/main/resources/models/apps/passthru/bubbleApp_passthru.json b/bubble-server/src/main/resources/models/apps/passthru/bubbleApp_passthru.json new file mode 100644 index 00000000..c50c0c58 --- /dev/null +++ b/bubble-server/src/main/resources/models/apps/passthru/bubbleApp_passthru.json @@ -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"} + ] + }] + } +}] \ No newline at end of file diff --git a/bubble-server/src/main/resources/models/apps/passthru/bubbleApp_passthru_matchers.json b/bubble-server/src/main/resources/models/apps/passthru/bubbleApp_passthru_matchers.json new file mode 100644 index 00000000..2d61128b --- /dev/null +++ b/bubble-server/src/main/resources/models/apps/passthru/bubbleApp_passthru_matchers.json @@ -0,0 +1,14 @@ +[{ + "name": "TlsPassthru", + "children": { + "AppMatcher": [{ + "name": "TlsPassthruMatcher", + "template": true, + "site": "All_Sites", + "fqdn": "*", + "urlRegex": ".*", + "rule": "passthru", + "priority": -1000000 + }] + } +}] \ No newline at end of file diff --git a/bubble-server/src/main/resources/models/apps/passthru/passthru-icon.svg b/bubble-server/src/main/resources/models/apps/passthru/passthru-icon.svg new file mode 100644 index 00000000..f1a99318 --- /dev/null +++ b/bubble-server/src/main/resources/models/apps/passthru/passthru-icon.svg @@ -0,0 +1,105 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/bubble-server/src/main/resources/models/apps/user_block/bubbleApp_userBlock.json b/bubble-server/src/main/resources/models/apps/user_block/bubbleApp_userBlock.json index 26554130..cf6253f0 100644 --- a/bubble-server/src/main/resources/models/apps/user_block/bubbleApp_userBlock.json +++ b/bubble-server/src/main/resources/models/apps/user_block/bubbleApp_userBlock.json @@ -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", diff --git a/bubble-server/src/main/resources/models/defaults/ruleDriver.json b/bubble-server/src/main/resources/models/defaults/ruleDriver.json index e75c5f65..113019f0 100644 --- a/bubble-server/src/main/resources/models/defaults/ruleDriver.json +++ b/bubble-server/src/main/resources/models/defaults/ruleDriver.json @@ -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": [] } } ] \ No newline at end of file diff --git a/bubble-web b/bubble-web index 72505216..7cd7e0e1 160000 --- a/bubble-web +++ b/bubble-web @@ -1 +1 @@ -Subproject commit 725052160cf969e5b2f041b0f764710e433feb7e +Subproject commit 7cd7e0e140da2d048ad3addfac4bceb72ab5e89a