@@ -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<String, String> params) { | |||||
switch (view) { | |||||
case VIEW_manageCookieReplacements: | |||||
return loadManageCookiesReplacements(account, app); | |||||
} | |||||
throw notFoundEx(view); | |||||
} | |||||
private Set<CookieReplacement> 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<String, String> 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<CookieReplacement> 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<String, String> 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<CookieReplacement> 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(); | |||||
} | |||||
} |
@@ -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 {} |
@@ -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<CookieReplacement> { | |||||
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()); | |||||
} | |||||
} |
@@ -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); | |||||
} | |||||
} |
@@ -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<CookieReplacement> 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; | |||||
} | |||||
} |
@@ -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 <C> Class<C> getConfigClass() { return (Class<C>) 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<CookieReplacement> 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); | |||||
} | |||||
} |
@@ -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" } | |||||
] | |||||
}] | |||||
} | |||||
}] |
@@ -0,0 +1,89 @@ | |||||
<?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:10;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" /> | |||||
</g> | |||||
</svg> |
@@ -1 +1 @@ | |||||
Subproject commit ea72ac4a1619c4f5915047650cdd18b8a6202681 | |||||
Subproject commit cad625431e357e94647a1d99da2efc171740d8e3 |