diff --git a/bubble-server/src/main/java/bubble/ApiConstants.java b/bubble-server/src/main/java/bubble/ApiConstants.java index 3ec73ac1..63296b98 100644 --- a/bubble-server/src/main/java/bubble/ApiConstants.java +++ b/bubble-server/src/main/java/bubble/ApiConstants.java @@ -212,6 +212,7 @@ public class ApiConstants { public static final String EP_NODE_MANAGER = "/nodeman"; public static final String EP_UPGRADE = "/upgrade"; public static final String EP_LOGS = "/logs"; + public static final String EP_FOLLOW = "/follow"; public static final String DETECT_ENDPOINT = "/detect"; public static final String EP_LOCALE = "/locale"; 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 eb7ae626..868f564b 100644 --- a/bubble-server/src/main/java/bubble/resources/stream/FilterHttpResource.java +++ b/bubble-server/src/main/java/bubble/resources/stream/FilterHttpResource.java @@ -33,6 +33,7 @@ import lombok.extern.slf4j.Slf4j; import org.cobbzilla.util.collection.ExpirationEvictionPolicy; import org.cobbzilla.util.collection.ExpirationMap; import org.cobbzilla.util.http.HttpContentEncodingType; +import org.cobbzilla.util.http.HttpUtil; import org.cobbzilla.util.network.NetworkUtil; import org.cobbzilla.wizard.cache.redis.RedisService; import org.glassfish.grizzly.http.server.Request; @@ -54,8 +55,7 @@ import static bubble.service.stream.HttpStreamDebug.getLogFqdn; import static bubble.service.stream.StandardRuleEngineService.MATCHERS_CACHE_TIMEOUT; import static com.google.common.net.HttpHeaders.CONTENT_SECURITY_POLICY; import static java.util.Collections.emptyMap; -import static java.util.concurrent.TimeUnit.HOURS; -import static java.util.concurrent.TimeUnit.MINUTES; +import static java.util.concurrent.TimeUnit.*; import static javax.ws.rs.core.HttpHeaders.CONTENT_LENGTH; import static org.cobbzilla.util.collection.ArrayUtil.arrayToString; import static org.cobbzilla.util.daemon.ZillaRuntime.*; @@ -630,6 +630,18 @@ public class FilterHttpResource { return ok_empty(); } + private final Map redirectCache + = new ExpirationMap<>(1000, DAYS.toMillis(3), ExpirationEvictionPolicy.atime); + + @POST @Path(EP_FOLLOW+"/{requestId}") + public Response followLink(@Context Request req, + @Context ContainerRequest ctx, + @PathParam("requestId") String requestId, + JsonNode urlNode) { + final FilterSubContext filterCtx = new FilterSubContext(req, requestId); + return ok(redirectCache.computeIfAbsent(urlNode.textValue(), HttpUtil::chaseRedirects)); + } + @Path(EP_ASSETS+"/{requestId}/{appId}") public AppAssetsResource getAppAssetsResource(@Context Request req, @Context ContainerRequest ctx, diff --git a/bubble-server/src/main/resources/bubble/rule/RequestModifierRule_icon.js.hbs b/bubble-server/src/main/resources/bubble/rule/RequestModifierRule_icon.js.hbs index 347aa980..61c304a6 100644 --- a/bubble-server/src/main/resources/bubble/rule/RequestModifierRule_icon.js.hbs +++ b/bubble-server/src/main/resources/bubble/rule/RequestModifierRule_icon.js.hbs @@ -57,6 +57,29 @@ function {{JS_PREFIX}}_create_button(labelKey, labelDefault, onclick, labelForma return btn; } +function {{JS_PREFIX}}_chase_redirects (a) { + if (a.className && a.className.indexOf('{{JS_PREFIX}}_followed') !== -1) return; + if (a.className) { + a.className = a.className + ' {{JS_PREFIX}}_followed'; + } else { + a.className = '{{JS_PREFIX}}_followed'; + } + + a.rel = 'noopener noreferrer nofollow'; + fetch('/__bubble/api/filter/follow/{{BUBBLE_REQUEST_ID}}', {method: 'POST', body: JSON.stringify(a.href)}) + .then(response => response.text()) + .then(data => { + if (data && (data.startsWith('http://') || data.startsWith('https://'))) { + a.href = data; + } else { + console.warn('chase_redirects: '+a.href+' returned non-URL response: '+data); + } + }) + .catch((error) => { + console.error('chase_redirects: error following: '+a.href+': '+error); + }); +} + if (typeof {{PAGE_PREFIX}}_icon_status === 'undefined') { {{PAGE_PREFIX}}_screenWidth = function () { return window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth }; diff --git a/bubble-server/src/main/resources/bubble/rule/social/block/site/FB.js.hbs b/bubble-server/src/main/resources/bubble/rule/social/block/site/FB.js.hbs index 28cdfeb3..35c5c16c 100644 --- a/bubble-server/src/main/resources/bubble/rule/social/block/site/FB.js.hbs +++ b/bubble-server/src/main/resources/bubble/rule/social/block/site/FB.js.hbs @@ -253,9 +253,9 @@ function {{JS_PREFIX}}_apply_blocks(blocked_users) { href = {{JS_PREFIX}}_remove_param(href, '__tn__'); href = {{JS_PREFIX}}_remove_param(href, 'refid'); a.href = href; - a.rel = 'noopener noreferrer nofollow'; a.removeAttribute('data-gt'); a.removeAttribute('data-sigil'); + {{JS_PREFIX}}_chase_redirects(a); }); } } diff --git a/bubble-server/src/main/resources/bubble/rule/social/block/site/Twitter.js.hbs b/bubble-server/src/main/resources/bubble/rule/social/block/site/Twitter.js.hbs index 644111f2..1de5aee3 100644 --- a/bubble-server/src/main/resources/bubble/rule/social/block/site/Twitter.js.hbs +++ b/bubble-server/src/main/resources/bubble/rule/social/block/site/Twitter.js.hbs @@ -68,6 +68,11 @@ function {{JS_PREFIX}}_apply_blocks(blocked_users) { continue; } else { // console.log('FOUND tweet node for author: '+authorName); + Array.from(tweet.getElementsByTagName('a')).forEach(a => { + if (a.href && a.href.indexOf('https://t.co/') !== -1) { + {{JS_PREFIX}}_chase_redirects(a); + } + }); } // have we visited this tweet before? diff --git a/utils/cobbzilla-utils b/utils/cobbzilla-utils index dcaedf85..2b96b840 160000 --- a/utils/cobbzilla-utils +++ b/utils/cobbzilla-utils @@ -1 +1 @@ -Subproject commit dcaedf8546b02e475e862657af3fab6cbbbb7754 +Subproject commit 2b96b8403583585c6d7013f7100d4d29064c408a