@@ -1,19 +1,24 @@ | |||||
package bubble.app.bblock; | package bubble.app.bblock; | ||||
import bubble.abp.BlockDecision; | |||||
import bubble.abp.BlockListSource; | import bubble.abp.BlockListSource; | ||||
import bubble.abp.BlockSpec; | import bubble.abp.BlockSpec; | ||||
import bubble.dao.app.AppRuleDAO; | import bubble.dao.app.AppRuleDAO; | ||||
import bubble.dao.app.RuleDriverDAO; | import bubble.dao.app.RuleDriverDAO; | ||||
import bubble.model.account.Account; | import bubble.model.account.Account; | ||||
import bubble.model.app.AppMatcher; | |||||
import bubble.model.app.AppRule; | import bubble.model.app.AppRule; | ||||
import bubble.model.app.BubbleApp; | import bubble.model.app.BubbleApp; | ||||
import bubble.model.app.RuleDriver; | import bubble.model.app.RuleDriver; | ||||
import bubble.model.app.config.AppConfigDriver; | import bubble.model.app.config.AppConfigDriver; | ||||
import bubble.model.device.Device; | |||||
import bubble.rule.bblock.BubbleBlockConfig; | import bubble.rule.bblock.BubbleBlockConfig; | ||||
import bubble.rule.bblock.BubbleBlockList; | import bubble.rule.bblock.BubbleBlockList; | ||||
import bubble.rule.bblock.BubbleBlockRuleDriver; | import bubble.rule.bblock.BubbleBlockRuleDriver; | ||||
import bubble.server.BubbleConfiguration; | |||||
import com.fasterxml.jackson.databind.JsonNode; | import com.fasterxml.jackson.databind.JsonNode; | ||||
import lombok.extern.slf4j.Slf4j; | import lombok.extern.slf4j.Slf4j; | ||||
import org.cobbzilla.util.string.ValidationRegexes; | |||||
import org.cobbzilla.wizard.validation.ValidationResult; | import org.cobbzilla.wizard.validation.ValidationResult; | ||||
import org.springframework.beans.factory.annotation.Autowired; | import org.springframework.beans.factory.annotation.Autowired; | ||||
@@ -25,6 +30,10 @@ import java.util.stream.Collectors; | |||||
import static java.util.Collections.emptyList; | import static java.util.Collections.emptyList; | ||||
import static org.cobbzilla.util.daemon.ZillaRuntime.*; | import static org.cobbzilla.util.daemon.ZillaRuntime.*; | ||||
import static org.cobbzilla.util.http.HttpSchemes.SCHEME_HTTPS; | |||||
import static org.cobbzilla.util.http.HttpSchemes.isHttpOrHttps; | |||||
import static org.cobbzilla.util.http.URIUtil.getHost; | |||||
import static org.cobbzilla.util.http.URIUtil.getPath; | |||||
import static org.cobbzilla.util.json.JsonUtil.json; | import static org.cobbzilla.util.json.JsonUtil.json; | ||||
import static org.cobbzilla.util.string.ValidationRegexes.HTTPS_PATTERN; | import static org.cobbzilla.util.string.ValidationRegexes.HTTPS_PATTERN; | ||||
import static org.cobbzilla.util.string.ValidationRegexes.HTTP_PATTERN; | import static org.cobbzilla.util.string.ValidationRegexes.HTTP_PATTERN; | ||||
@@ -38,9 +47,12 @@ public class BubbleBlockAppConfigDriver implements AppConfigDriver { | |||||
public static final String VIEW_manageLists = "manageLists"; | public static final String VIEW_manageLists = "manageLists"; | ||||
public static final String VIEW_manageList = "manageList"; | public static final String VIEW_manageList = "manageList"; | ||||
public static final String VIEW_manageRules = "manageRules"; | public static final String VIEW_manageRules = "manageRules"; | ||||
public static final AppMatcher TEST_MATCHER = new AppMatcher(); | |||||
public static final Device TEST_DEVICE = new Device(); | |||||
@Autowired private RuleDriverDAO driverDAO; | @Autowired private RuleDriverDAO driverDAO; | ||||
@Autowired private AppRuleDAO ruleDAO; | @Autowired private AppRuleDAO ruleDAO; | ||||
@Autowired private BubbleConfiguration configuration; | |||||
@Override public Object getView(Account account, BubbleApp app, String view, Map<String, String> params) { | @Override public Object getView(Account account, BubbleApp app, String view, Map<String, String> params) { | ||||
final String id = params.get(PARAM_ID); | final String id = params.get(PARAM_ID); | ||||
@@ -114,10 +126,12 @@ public class BubbleBlockAppConfigDriver implements AppConfigDriver { | |||||
public static final String ACTION_updateList = "updateList"; | public static final String ACTION_updateList = "updateList"; | ||||
public static final String ACTION_createRule = "createRule"; | public static final String ACTION_createRule = "createRule"; | ||||
public static final String ACTION_removeRule = "removeRule"; | public static final String ACTION_removeRule = "removeRule"; | ||||
public static final String ACTION_test_url = "test_url"; | |||||
public static final String ACTION_testUrl = "testUrl"; | |||||
public static final String PARAM_URL = "url"; | public static final String PARAM_URL = "url"; | ||||
public static final String PARAM_RULE = "rule"; | public static final String PARAM_RULE = "rule"; | ||||
public static final String PARAM_TEST_URL = "testUrl"; | |||||
public static final String PARAM_TEST_URL_PRIMARY = "testUrlPrimary"; | |||||
@Override public Object takeAppAction(Account account, | @Override public Object takeAppAction(Account account, | ||||
BubbleApp app, | BubbleApp app, | ||||
@@ -130,10 +144,47 @@ public class BubbleBlockAppConfigDriver implements AppConfigDriver { | |||||
return addList(account, app, data); | return addList(account, app, data); | ||||
case ACTION_createRule: | case ACTION_createRule: | ||||
return addRule(account, app, params, data); | return addRule(account, app, params, data); | ||||
case ACTION_testUrl: | |||||
return testUrl(account, app, data); | |||||
} | } | ||||
throw notFoundEx(action); | throw notFoundEx(action); | ||||
} | } | ||||
private BubbleBlockList testUrl(Account account, BubbleApp app, JsonNode data) { | |||||
final JsonNode testUrlNode = data.get(PARAM_TEST_URL); | |||||
if (testUrlNode == null || empty(testUrlNode.textValue())) throw invalidEx("err.testUrl.required"); | |||||
String testUrl = testUrlNode.textValue(); | |||||
final JsonNode testUrlPrimaryNode = data.get(PARAM_TEST_URL_PRIMARY); | |||||
final boolean primary = testUrlPrimaryNode == null || testUrlPrimaryNode.booleanValue(); | |||||
if (!isHttpOrHttps(testUrl)) testUrl = SCHEME_HTTPS + testUrl; | |||||
final String host; | |||||
final String path; | |||||
try { | |||||
host = getHost(testUrl); | |||||
path = getPath(testUrl); | |||||
} catch (Exception e) { | |||||
throw invalidEx("err.testUrl.invalid", "Test URL was not valid", shortError(e)); | |||||
} | |||||
if (empty(host) || !ValidationRegexes.HOST_PATTERN.matcher(host).matches()) { | |||||
throw invalidEx("err.testUrl.invalidHostname", "Test URL was not valid"); | |||||
} | |||||
try { | |||||
final AppRule rule = loadRule(account, app); | |||||
final RuleDriver ruleDriver = loadDriver(account, rule); | |||||
final BubbleBlockRuleDriver unwiredDriver = (BubbleBlockRuleDriver) rule.initDriver(ruleDriver, TEST_MATCHER, account, TEST_DEVICE); | |||||
final BubbleBlockRuleDriver driver = configuration.autowire(unwiredDriver); | |||||
final BlockDecision decision = driver.getDecision(host, path, primary); | |||||
return getBuiltinList(account, app).setResponse(decision); | |||||
} catch (Exception e) { | |||||
throw invalidEx("err.testRule.loadingTestDriver", "Error loading test driver", shortError(e)); | |||||
} | |||||
} | |||||
private BubbleBlockList addRule(Account account, BubbleApp app, Map<String, String> params, JsonNode data) { | private BubbleBlockList addRule(Account account, BubbleApp app, Map<String, String> params, JsonNode data) { | ||||
final String id = params.get(PARAM_ID); | final String id = params.get(PARAM_ID); | ||||
@@ -151,7 +202,8 @@ public class BubbleBlockAppConfigDriver implements AppConfigDriver { | |||||
} | } | ||||
} | } | ||||
try { | try { | ||||
BlockSpec.parse(line); | |||||
final List<BlockSpec> specs = BlockSpec.parse(line); | |||||
if (log.isDebugEnabled()) log.debug("addRule: parsed line ("+line+"): "+json(specs)); | |||||
} catch (Exception e) { | } catch (Exception e) { | ||||
log.warn("addRule: invalid line ("+line+"): "+shortError(e)); | log.warn("addRule: invalid line ("+line+"): "+shortError(e)); | ||||
throw invalidEx("err.rule.invalid", "Error parsing rule", e.getMessage()); | throw invalidEx("err.rule.invalid", "Error parsing rule", e.getMessage()); | ||||
@@ -233,11 +285,15 @@ public class BubbleBlockAppConfigDriver implements AppConfigDriver { | |||||
} | } | ||||
private BubbleBlockList removeRule(Account account, BubbleApp app, String id) { | private BubbleBlockList removeRule(Account account, BubbleApp app, String id) { | ||||
final BubbleBlockList builtin = getBuiltinList(account, app); | |||||
return updateList(builtin.removeRule(id)); | |||||
} | |||||
private BubbleBlockList getBuiltinList(Account account, BubbleApp app) { | |||||
final List<BubbleBlockList> customLists = loadAllLists(account, app).stream().filter(list -> !list.hasUrl()).collect(Collectors.toList()); | final List<BubbleBlockList> customLists = loadAllLists(account, app).stream().filter(list -> !list.hasUrl()).collect(Collectors.toList()); | ||||
if (customLists.isEmpty()) throw invalidEx("err.removeRule.noCustomList"); | if (customLists.isEmpty()) throw invalidEx("err.removeRule.noCustomList"); | ||||
if (customLists.size() > 1) throw invalidEx("err.removeRule.multipleCustomLists"); | if (customLists.size() > 1) throw invalidEx("err.removeRule.multipleCustomLists"); | ||||
final BubbleBlockList builtin = customLists.get(0); | |||||
return updateList(builtin.removeRule(id)); | |||||
return customLists.get(0); | |||||
} | } | ||||
private ValidationResult validate(BubbleBlockList list, BubbleBlockList request, List<BubbleBlockList> allLists) { | private ValidationResult validate(BubbleBlockList list, BubbleBlockList request, List<BubbleBlockList> allLists) { | ||||
@@ -12,6 +12,7 @@ public class AppConfigAction { | |||||
@Getter @Setter private String when; | @Getter @Setter private String when; | ||||
@Getter @Setter private String view; | @Getter @Setter private String view; | ||||
@Getter @Setter private String successView; | @Getter @Setter private String successView; | ||||
@Getter @Setter private String successMessage; | |||||
@Getter @Setter private Integer index = 0; | @Getter @Setter private Integer index = 0; | ||||
@Getter @Setter private String[] params; | @Getter @Setter private String[] params; | ||||
@@ -11,6 +11,7 @@ import lombok.experimental.Accessors; | |||||
import lombok.extern.slf4j.Slf4j; | import lombok.extern.slf4j.Slf4j; | ||||
import org.cobbzilla.util.collection.ArrayUtil; | import org.cobbzilla.util.collection.ArrayUtil; | ||||
import javax.persistence.Transient; | |||||
import java.util.Arrays; | import java.util.Arrays; | ||||
import java.util.List; | import java.util.List; | ||||
import java.util.stream.Collectors; | import java.util.stream.Collectors; | ||||
@@ -66,6 +67,8 @@ public class BubbleBlockList { | |||||
@JsonIgnore @Getter @Setter private AppRule rule; | @JsonIgnore @Getter @Setter private AppRule rule; | ||||
@Transient @Getter @Setter private Object response; // non-standard config response (test URL) uses this | |||||
public boolean hasEntry(String line) { | public boolean hasEntry(String line) { | ||||
return hasAdditionalEntries() && Arrays.asList(getAdditionalEntries()).contains(line); | return hasAdditionalEntries() && Arrays.asList(getAdditionalEntries()).contains(line); | ||||
} | } | ||||
@@ -75,13 +75,14 @@ public class BubbleBlockRuleDriver extends TrafficAnalyticsRuleDriver { | |||||
} | } | ||||
} | } | ||||
if (list.hasAdditionalEntries()) { | if (list.hasAdditionalEntries()) { | ||||
if (blockListSource == null) blockListSource = new BlockListSource(); // might be built-in source | |||||
try { | try { | ||||
blockListSource.addEntries(list.getAdditionalEntries()); | blockListSource.addEntries(list.getAdditionalEntries()); | ||||
} catch (IOException e) { | } catch (IOException e) { | ||||
log.error("init: error adding additional entries: "+shortError(e)); | log.error("init: error adding additional entries: "+shortError(e)); | ||||
} | } | ||||
} | } | ||||
blockList.merge(blockListSource.getBlockList()); | |||||
if (blockListSource != null) blockList.merge(blockListSource.getBlockList()); | |||||
} | } | ||||
} | } | ||||
@@ -95,7 +96,7 @@ public class BubbleBlockRuleDriver extends TrafficAnalyticsRuleDriver { | |||||
final String site = ruleHarness.getMatcher().getSite(); | final String site = ruleHarness.getMatcher().getSite(); | ||||
final String fqdn = filter.getFqdn(); | final String fqdn = filter.getFqdn(); | ||||
final BlockDecision decision = blockList.getDecision(filter.getFqdn(), filter.getUri()); | |||||
final BlockDecision decision = getDecision(filter.getFqdn(), filter.getUri()); | |||||
switch (decision.getDecisionType()) { | switch (decision.getDecisionType()) { | ||||
case block: | case block: | ||||
incrementCounters(account, device, app, site, fqdn); | incrementCounters(account, device, app, site, fqdn); | ||||
@@ -107,6 +108,10 @@ public class BubbleBlockRuleDriver extends TrafficAnalyticsRuleDriver { | |||||
} | } | ||||
} | } | ||||
public BlockDecision getDecision(String fqdn, String uri) { return blockList.getDecision(fqdn, uri, false); } | |||||
public BlockDecision getDecision(String fqdn, String uri, boolean primary) { return blockList.getDecision(fqdn, uri, primary); } | |||||
public FilterMatchResponse getFilterMatchResponse(FilterMatchersRequest filter, BlockDecision decision) { | public FilterMatchResponse getFilterMatchResponse(FilterMatchersRequest filter, BlockDecision decision) { | ||||
switch (decision.getDecisionType()) { | switch (decision.getDecisionType()) { | ||||
case block: return FilterMatchResponse.ABORT_NOT_FOUND; | case block: return FilterMatchResponse.ABORT_NOT_FOUND; | ||||
@@ -144,7 +149,7 @@ public class BubbleBlockRuleDriver extends TrafficAnalyticsRuleDriver { | |||||
} | } | ||||
// Now that we know the content type, re-check the BlockList | // Now that we know the content type, re-check the BlockList | ||||
final BlockDecision decision = blockList.getDecision(request.getFqdn(), request.getUri(), contentType); | |||||
final BlockDecision decision = blockList.getDecision(request.getFqdn(), request.getUri(), contentType, true); | |||||
switch (decision.getDecisionType()) { | switch (decision.getDecisionType()) { | ||||
case block: | case block: | ||||
log.warn("doFilterRequest: preprocessed request was filtered, but ultimate decision was block, returning EMPTY_STREAM"); | log.warn("doFilterRequest: preprocessed request was filtered, but ultimate decision was block, returning EMPTY_STREAM"); | ||||
@@ -212,7 +212,7 @@ public class RuleEngineService { | |||||
for (AppRuleHarness h : rules) { | for (AppRuleHarness h : rules) { | ||||
final RuleDriver ruleDriver = driverDAO.findByUuid(h.getRule().getDriver()); | final RuleDriver ruleDriver = driverDAO.findByUuid(h.getRule().getDriver()); | ||||
if (ruleDriver == null) { | if (ruleDriver == null) { | ||||
log.warn("get: driver not found: "+h.getRule().getDriver()); | |||||
log.warn("initRules: driver not found: "+h.getRule().getDriver()); | |||||
continue; | continue; | ||||
} | } | ||||
final AppRuleDriver unwiredDriver = h.getRule().initDriver(ruleDriver, h.getMatcher(), account, device); | final AppRuleDriver unwiredDriver = h.getRule().initDriver(ruleDriver, h.getMatcher(), account, device); | ||||
@@ -614,6 +614,10 @@ err.suspended.cannotSuspendSelf=You cannot suspend yourself | |||||
err.tag.invalid=Tag is invalid | err.tag.invalid=Tag is invalid | ||||
err.tagsJson.length=Too many tags | err.tagsJson.length=Too many tags | ||||
err.tagString.length=Too many tags | err.tagString.length=Too many tags | ||||
err.testUrl.required=URL is required | |||||
err.testUrl.loadingTestDriver=Error loading test driver | |||||
err.testUrl.invalid=URL is invalid | |||||
err.testUrl.invalidHostname=URL did not have a valid hostname | |||||
err.tgzB64.invalid.noRolesDir=No roles directory found in tgz | err.tgzB64.invalid.noRolesDir=No roles directory found in tgz | ||||
err.tgzB64.invalid.wrongNumberOfFiles=Wrong number of files in tgz base directory | err.tgzB64.invalid.wrongNumberOfFiles=Wrong number of files in tgz base directory | ||||
err.tgzB64.invalid.missingTasksMainYml=No tasks/main.yml file found for role in tgz | err.tgzB64.invalid.missingTasksMainYml=No tasks/main.yml file found for role in tgz | ||||
@@ -28,12 +28,13 @@ | |||||
{"name": "name"}, | {"name": "name"}, | ||||
{"name": "description", "control": "textarea"}, | {"name": "description", "control": "textarea"}, | ||||
{"name": "url", "type": "http_url"}, | {"name": "url", "type": "http_url"}, | ||||
{"name": "testUrl", "type": "http_url"}, | |||||
{"name": "tags"}, | {"name": "tags"}, | ||||
{"name": "tagString"}, | {"name": "tagString"}, | ||||
{"name": "enabled", "type": "flag", "mode": "readOnly"}, | {"name": "enabled", "type": "flag", "mode": "readOnly"}, | ||||
{"name": "rule"}, | {"name": "rule"}, | ||||
{"name": "ruleType", "mode": "readOnly"} | |||||
{"name": "ruleType", "mode": "readOnly"}, | |||||
{"name": "testUrl", "type": "http_url"}, | |||||
{"name": "testUrlPrimary", "type": "flag"} | |||||
], | ], | ||||
"configViews": [{ | "configViews": [{ | ||||
"name": "manageLists", | "name": "manageLists", | ||||
@@ -53,8 +54,9 @@ | |||||
}, | }, | ||||
{ | { | ||||
"name": "testUrl", "scope": "app", "index": 20, | "name": "testUrl", "scope": "app", "index": 20, | ||||
"params": ["testUrl"], | |||||
"button": "testUrl" | |||||
"params": ["testUrl", "testUrlPrimary"], | |||||
"button": "testUrl", | |||||
"successMessage": "response.decisionType" | |||||
} | } | ||||
] | ] | ||||
}, { | }, { | ||||
@@ -81,8 +83,9 @@ | |||||
}, | }, | ||||
{ | { | ||||
"name": "testUrl", "scope": "app", "index": 20, | "name": "testUrl", "scope": "app", "index": 20, | ||||
"params": ["testUrl"], | |||||
"button": "testUrl" | |||||
"params": ["testUrl", "testUrlPrimary"], | |||||
"button": "testUrl", | |||||
"successMessage": "decisionType" | |||||
} | } | ||||
] | ] | ||||
}] | }] | ||||
@@ -159,6 +162,8 @@ | |||||
{"name": "config.field.ruleType", "value": "Rule Type"}, | {"name": "config.field.ruleType", "value": "Rule Type"}, | ||||
{"name": "config.field.testUrl", "value": "Test URL"}, | {"name": "config.field.testUrl", "value": "Test URL"}, | ||||
{"name": "config.field.testUrl.description", "value": "URL to check against filters"}, | {"name": "config.field.testUrl.description", "value": "URL to check against filters"}, | ||||
{"name": "config.field.testUrlPrimary", "value": "Primary"}, | |||||
{"name": "config.field.testUrlPrimary.description", "value": "A primary request will receive either an ALLOW or BLOCK decision from your Bubble. A non-primary request (for example a request for a webpage) may additionally receive a FILTER decision. This means the request will be permitted, but the response will be instrumented with Bubble filters to remove ads, malware and blocked elements."}, | |||||
{"name": "config.action.enableList", "value": "Enable"}, | {"name": "config.action.enableList", "value": "Enable"}, | ||||
{"name": "config.action.disableList", "value": "Disable"}, | {"name": "config.action.disableList", "value": "Disable"}, | ||||
@@ -172,7 +177,13 @@ | |||||
{"name": "config.action.createRule", "value": "Add New Rule"}, | {"name": "config.action.createRule", "value": "Add New Rule"}, | ||||
{"name": "config.button.createRule", "value": "Add"}, | {"name": "config.button.createRule", "value": "Add"}, | ||||
{"name": "config.action.testUrl", "value": "Test URL"}, | {"name": "config.action.testUrl", "value": "Test URL"}, | ||||
{"name": "config.button.testUrl", "value": "Test"} | |||||
{"name": "config.button.testUrl", "value": "Test"}, | |||||
{"name": "config.response.block", "value": "Block"}, | |||||
{"name": "config.response.block.description", "value": "Requests to this URL would be blocked by your Bubble"}, | |||||
{"name": "config.response.allow", "value": "Allow"}, | |||||
{"name": "config.response.allow.description", "value": "Requests to this URL would be allowed by your Bubble, and would not be filtered"}, | |||||
{"name": "config.response.filter", "value": "Filter"}, | |||||
{"name": "config.response.filter.description", "value": "Requests to this URL would be allowed by your Bubble, but would be filtered"} | |||||
] | ] | ||||
}] | }] | ||||
} | } |
@@ -1 +1 @@ | |||||
Subproject commit 66eab3f6c7013af92ab0aa8e0bd24855edaf986a | |||||
Subproject commit e25dbdfc8a4d6e617b17a4e5347d0cd143425576 |
@@ -1 +1 @@ | |||||
Subproject commit 3922106b4227b8fa2e77eeea113e5cba2308c08b | |||||
Subproject commit 5f96e9be2a720925347ba65c866a1cee2cdb4cd5 |