From 447b06b2e3204540f0f40b8f1b7d4e24f632b677 Mon Sep 17 00:00:00 2001 From: Kristijan Mitrovic Date: Wed, 16 Sep 2020 14:36:22 +0200 Subject: [PATCH] Add RequestProtector app --- .../RequestProtectorAppConfigDriver.java | 115 ++++++++++++++++++ .../RequestProtectorAppDataDriver.java | 9 ++ .../rule/request/CookieReplacement.java | 30 +++++ .../request/HttpHeaderReplacementFilter.java | 18 +++ .../rule/request/RequestProtectorConfig.java | 37 ++++++ .../request/RequestProtectorRuleDriver.java | 54 ++++++++ .../apps/request/bubbleApp_request.json | 75 ++++++++++++ .../models/apps/request/request-icon.svg | 89 ++++++++++++++ utils/cobbzilla-utils | 2 +- 9 files changed, 428 insertions(+), 1 deletion(-) create mode 100644 bubble-server/src/main/java/bubble/app/request/RequestProtectorAppConfigDriver.java create mode 100644 bubble-server/src/main/java/bubble/app/request/RequestProtectorAppDataDriver.java create mode 100644 bubble-server/src/main/java/bubble/rule/request/CookieReplacement.java create mode 100644 bubble-server/src/main/java/bubble/rule/request/HttpHeaderReplacementFilter.java create mode 100644 bubble-server/src/main/java/bubble/rule/request/RequestProtectorConfig.java create mode 100644 bubble-server/src/main/java/bubble/rule/request/RequestProtectorRuleDriver.java create mode 100644 bubble-server/src/main/resources/models/apps/request/bubbleApp_request.json create mode 100644 bubble-server/src/main/resources/models/apps/request/request-icon.svg diff --git a/bubble-server/src/main/java/bubble/app/request/RequestProtectorAppConfigDriver.java b/bubble-server/src/main/java/bubble/app/request/RequestProtectorAppConfigDriver.java new file mode 100644 index 00000000..21af38fe --- /dev/null +++ b/bubble-server/src/main/java/bubble/app/request/RequestProtectorAppConfigDriver.java @@ -0,0 +1,115 @@ +/** + * Copyright (c) 2020 Bubble, Inc. All rights reserved. + * For personal (non-commercial) use, see license: https://getbubblenow.com/bubble-license/ + */ +package bubble.app.request; + +import bubble.model.account.Account; +import bubble.model.app.AppRule; +import bubble.model.app.BubbleApp; +import bubble.model.app.config.AppConfigDriverBase; +import bubble.rule.request.CookieReplacement; +import bubble.rule.request.RequestProtectorConfig; +import bubble.rule.request.RequestProtectorRuleDriver; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; +import java.util.Set; + +import static org.cobbzilla.util.daemon.ZillaRuntime.empty; +import static org.cobbzilla.util.json.JsonUtil.json; +import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx; +import static org.cobbzilla.wizard.resources.ResourceUtil.notFoundEx; + +@Slf4j +public class RequestProtectorAppConfigDriver extends AppConfigDriverBase { + + public static final String VIEW_manageCookieReplacements = "manageCookieReplacements"; + + @Override public Object getView(Account account, BubbleApp app, String view, Map params) { + switch (view) { + case VIEW_manageCookieReplacements: + return loadManageCookiesReplacements(account, app); + } + throw notFoundEx(view); + } + + private Set loadManageCookiesReplacements(Account account, BubbleApp app) { + final RequestProtectorConfig config = getConfig(account, app); + return config.getCookieReplacements(); + } + + private RequestProtectorConfig getConfig(Account account, BubbleApp app) { + return getConfig(account, app, RequestProtectorRuleDriver.class, RequestProtectorConfig.class); + } + + public static final String ACTION_addCookieReplacement = "addCookieReplacement"; + public static final String ACTION_removeCookieReplacement = "removeCookieReplacement"; + + public static final String PARAM_REGEX = "regex"; + public static final String PARAM_REPLACEMENT = "replacement"; + + @Override public Object takeAppAction(Account account, BubbleApp app, String view, String action, + Map params, JsonNode data) { + switch (action) { + case ACTION_addCookieReplacement: + return addCookieReplacement(account, app, data); + } + if (log.isWarnEnabled()) log.warn("takeAppAction: action not found: "+action); + throw notFoundEx(action); + } + + private Set addCookieReplacement(Account account, BubbleApp app, JsonNode data) { + final JsonNode regexNode = data.get(PARAM_REGEX); + if (regexNode == null || regexNode.textValue() == null || empty(regexNode.textValue().trim())) { + throw invalidEx("err.requestProtector.cookieRegexRequired"); + } + final String regex = regexNode.textValue().trim().toLowerCase(); + + final JsonNode replacementNode = data.get(PARAM_REPLACEMENT); + final String replacement = replacementNode == null || replacementNode.textValue() == null + ? "" + : replacementNode.textValue().trim().toLowerCase(); + + final RequestProtectorConfig config = getConfig(account, app).addCookieReplacement(regex, replacement); + + final AppRule rule = loadRule(account, app); + loadDriver(account, rule, RequestProtectorRuleDriver.class); // validate proper driver + if (log.isDebugEnabled()) { + log.debug("addCookieReplacement: updating rule: " + rule.getName() + ", adding regex: " + regex); + } + ruleDAO.update(rule.setConfigJson(json(config))); + + return config.getCookieReplacements(); + } + + @Override public Object takeItemAction(Account account, BubbleApp app, String view, String action, String id, + Map params, JsonNode data) { + switch (action) { + case ACTION_removeCookieReplacement: + return removeCookieReplacement(account, app, id); + } + if (log.isWarnEnabled()) log.warn("takeItemAction: action not found: "+action); + throw notFoundEx(action); + } + + private Set removeCookieReplacement(Account account, BubbleApp app, String regex) { + final AppRule rule = loadRule(account, app); + loadDriver(account, rule, RequestProtectorRuleDriver.class); // validate proper driver + final RequestProtectorConfig config = getConfig(account, app); + if (log.isDebugEnabled()) { + log.debug("removeCookieReplacement: removing regex: " + regex + " from config.cookiesReplacements: " + + config.getCookieReplacements().toString()); + } + + final RequestProtectorConfig updated = config.removeCookieReplacement(regex); + if (log.isDebugEnabled()) { + log.debug("removeCookieReplacement: updated.cookiesReplacements: " + + updated.getCookieReplacements().toString()); + } + ruleDAO.update(rule.setConfigJson(json(updated))); + + return updated.getCookieReplacements(); + } +} diff --git a/bubble-server/src/main/java/bubble/app/request/RequestProtectorAppDataDriver.java b/bubble-server/src/main/java/bubble/app/request/RequestProtectorAppDataDriver.java new file mode 100644 index 00000000..d072acda --- /dev/null +++ b/bubble-server/src/main/java/bubble/app/request/RequestProtectorAppDataDriver.java @@ -0,0 +1,9 @@ +/** + * Copyright (c) 2020 Bubble, Inc. All rights reserved. + * For personal (non-commercial) use, see license: https://getbubblenow.com/bubble-license/ + */ +package bubble.app.request; + +import bubble.model.app.config.AppDataDriverBase; + +public class RequestProtectorAppDataDriver extends AppDataDriverBase {} diff --git a/bubble-server/src/main/java/bubble/rule/request/CookieReplacement.java b/bubble-server/src/main/java/bubble/rule/request/CookieReplacement.java new file mode 100644 index 00000000..5318214c --- /dev/null +++ b/bubble-server/src/main/java/bubble/rule/request/CookieReplacement.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2020 Bubble, Inc. All rights reserved. + * For personal (non-commercial) use, see license: https://getbubblenow.com/bubble-license/ + */ +package bubble.rule.request; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import lombok.Setter; +import lombok.experimental.Accessors; + +@NoArgsConstructor @Accessors(chain=true) +public class CookieReplacement implements Comparable { + + public String getId() { return regex; } + public void setId(String id) {} // noop + + @Getter @Setter private String regex; + @Getter @Setter private String replacement; + + public CookieReplacement(@NonNull final String regex, @NonNull final String replacement) { + this.regex = regex; + this.replacement = replacement; + } + + @Override public int compareTo(@NonNull final CookieReplacement o) { + return getRegex().compareTo(o.getRegex().toLowerCase()); + } +} diff --git a/bubble-server/src/main/java/bubble/rule/request/HttpHeaderReplacementFilter.java b/bubble-server/src/main/java/bubble/rule/request/HttpHeaderReplacementFilter.java new file mode 100644 index 00000000..51837b28 --- /dev/null +++ b/bubble-server/src/main/java/bubble/rule/request/HttpHeaderReplacementFilter.java @@ -0,0 +1,18 @@ +package bubble.rule.request; + +import org.cobbzilla.util.io.regex.RegexLimitedReplacementFilter; + +import java.util.regex.Pattern; + +public class HttpHeaderReplacementFilter extends RegexLimitedReplacementFilter { + private static final String HTTP_HEADER_BORDER_REGEX = "\r?\n\r?\n"; + + private HttpHeaderReplacementFilter(String regex, int group, String replacement, String stopRegex, + int stopRegexMatchingFlags) { + // should not be used + } + + public HttpHeaderReplacementFilter(String regex, String replacement) { + super(regex, 0, replacement, HTTP_HEADER_BORDER_REGEX, Pattern.MULTILINE); + } +} diff --git a/bubble-server/src/main/java/bubble/rule/request/RequestProtectorConfig.java b/bubble-server/src/main/java/bubble/rule/request/RequestProtectorConfig.java new file mode 100644 index 00000000..39ae5bac --- /dev/null +++ b/bubble-server/src/main/java/bubble/rule/request/RequestProtectorConfig.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2020 Bubble, Inc. All rights reserved. + * For personal (non-commercial) use, see license: https://getbubblenow.com/bubble-license/ + */ +package bubble.rule.request; + +import lombok.Getter; +import lombok.NonNull; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.extern.slf4j.Slf4j; + +import java.util.Set; +import java.util.TreeSet; + +import static org.cobbzilla.util.daemon.ZillaRuntime.empty; + +@Slf4j @Accessors(chain=true) +public class RequestProtectorConfig { + + @Getter @Setter private Set cookieReplacements = new TreeSet<>(); + public boolean hasCookieReplacements() { return !empty(cookieReplacements); } + public boolean hasCookieReplacementFor(@NonNull final String regex) { + return hasCookieReplacements() && cookieReplacements.stream().anyMatch(r -> r.getRegex().equals(regex)); + } + + @NonNull public RequestProtectorConfig addCookieReplacement(@NonNull final String regex, + @NonNull final String replacement) { + cookieReplacements.add(new CookieReplacement(regex, replacement)); + return this; + } + + @NonNull public RequestProtectorConfig removeCookieReplacement(@NonNull final String regex) { + if (hasCookieReplacements()) cookieReplacements.removeIf(r -> r.getRegex().equals(regex)); + return this; + } +} diff --git a/bubble-server/src/main/java/bubble/rule/request/RequestProtectorRuleDriver.java b/bubble-server/src/main/java/bubble/rule/request/RequestProtectorRuleDriver.java new file mode 100644 index 00000000..9dfad25d --- /dev/null +++ b/bubble-server/src/main/java/bubble/rule/request/RequestProtectorRuleDriver.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2020 Bubble, Inc. All rights reserved. + * For personal (non-commercial) use, see license: https://getbubblenow.com/bubble-license/ + */ +package bubble.rule.request; + +import bubble.model.account.Account; +import bubble.model.app.AppMatcher; +import bubble.model.app.AppRule; +import bubble.model.app.BubbleApp; +import bubble.model.device.Device; +import bubble.resources.stream.FilterHttpRequest; +import bubble.rule.AbstractAppRuleDriver; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.input.ReaderInputStream; +import org.cobbzilla.util.io.regex.RegexFilterReader; + +import java.io.InputStream; +import java.util.Iterator; + +import static org.cobbzilla.util.string.StringUtil.UTF8cs; + +@Slf4j +public class RequestProtectorRuleDriver extends AbstractAppRuleDriver { + + @Override public Class getConfigClass() { return (Class) RequestProtectorConfig.class; } + + @Override public void init(JsonNode config, JsonNode userConfig, BubbleApp app, AppRule rule, AppMatcher matcher, + Account account, Device device) { + super.init(config, userConfig, app, rule, matcher, account, device); + + // refresh list + final RequestProtectorConfig ruleConfig = getRuleConfig(); + ruleConfig.getCookieReplacements(); + } + + @Override public InputStream doFilterResponse(FilterHttpRequest filterRequest, InputStream in) { + final RequestProtectorConfig config = getRuleConfig(); + if (!config.hasCookieReplacements()) return in; + + final Iterator crIterator = config.getCookieReplacements().iterator(); + CookieReplacement cr = crIterator.next(); + RegexFilterReader reader = new RegexFilterReader(in, new HttpHeaderReplacementFilter(cr.getRegex(), + cr.getReplacement())); + while (crIterator.hasNext()) { + cr = crIterator.next(); + reader = new RegexFilterReader(reader, new HttpHeaderReplacementFilter(cr.getRegex(), + cr.getReplacement())); + } + + return new ReaderInputStream(reader, UTF8cs); + } +} diff --git a/bubble-server/src/main/resources/models/apps/request/bubbleApp_request.json b/bubble-server/src/main/resources/models/apps/request/bubbleApp_request.json new file mode 100644 index 00000000..6990f10d --- /dev/null +++ b/bubble-server/src/main/resources/models/apps/request/bubbleApp_request.json @@ -0,0 +1,75 @@ +[{ + "name": "RequestProtector", + "description": "Change or remove parts of request/response - i.e. remove cross-domain cookies from response", + "url": "https://getbubblenow.com/apps/request", + "template": true, + "enabled": true, + "priority": 1000, + "canPrime": true, + "dataConfig": { + "dataDriver": "bubble.app.request.RequestProtectorAppDataDriver", + "presentation": "none", + "configDriver": "bubble.app.request.RequestProtectorAppConfigDriver", + "configFields": [ + {"name": "regex", "truncate": false}, + {"name": "replacement", "truncate": false} + ], + "configViews": [{ + "name": "manageCookieReplacements", + "scope": "app", + "root": "true", + "fields": [ "regex", "replacement" ], + "actions": [ + {"name": "removeCookieReplacement", "index": 10}, + { + "name": "addCookieReplacement", "scope": "app", "index": 10, + "params": [ "regex", "replacement" ], + "button": "addCookieReplacement" + } + ] + }] + }, + "children": { + "AppSite": [{ + "name": "All_Sites", + "url": "*", + "description": "All websites", + "template": true + }], + "AppRule": [{ + "name": "request", + "template": true, + "driver": "RequestProtectorRuleDriver", + "priority": -1000, + "config": { "cookieReplacements": [] } + }], + "AppMessage": [{ + "locale": "en_US", + "messages": [ + { "name": "name", "value": "RequestProtector" }, + { "name": "icon", "value": "classpath:models/apps/request/request-icon.svg" }, + { "name": "summary", "value": "Request Protector" }, + { + "name": "description", + "value": "Change or remove parts of request/response - i.e. remove cross-domain cookies from response" + }, + + { "name": "config.view.manageCookieReplacements", "value": "Manage Cookie Replacements" }, + { "name": "config.field.regex", "value": "RegEx" }, + { + "name": "config.field.regex.description", + "value": "Regular expression compared with full set cookie string value" + }, + { "name": "config.field.replacement", "value": "Replacement" }, + { + "name": "config.field.replacement.description", + "value": "May use reference from regex as in Java's replaceAll method" + }, + { "name": "config.action.addCookieReplacement", "value": "Add" }, + { "name": "config.action.removeCookieReplacement", "value": "Remove" }, + + { "name": "err.requestProtector.cookieRegexRequired", "value": "RegEx field is required" } + ] + }] + } +}] \ No newline at end of file diff --git a/bubble-server/src/main/resources/models/apps/request/request-icon.svg b/bubble-server/src/main/resources/models/apps/request/request-icon.svg new file mode 100644 index 00000000..5c29e3a5 --- /dev/null +++ b/bubble-server/src/main/resources/models/apps/request/request-icon.svg @@ -0,0 +1,89 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/utils/cobbzilla-utils b/utils/cobbzilla-utils index ea72ac4a..cad62543 160000 --- a/utils/cobbzilla-utils +++ b/utils/cobbzilla-utils @@ -1 +1 @@ -Subproject commit ea72ac4a1619c4f5915047650cdd18b8a6202681 +Subproject commit cad625431e357e94647a1d99da2efc171740d8e3