Fix .onion link linkification.

Fixes #11458.
This commit is contained in:
Justin Tracey
2022-04-14 17:58:05 -04:00
committed by Greyson Parrelli
parent 6563ea970f
commit 8a2f89b4f6
8 changed files with 108 additions and 77 deletions

View File

@@ -11,6 +11,7 @@ import androidx.core.util.Consumer;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.signal.core.util.Hex;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.InvalidMessageException;
@@ -45,8 +46,8 @@ import org.thoughtcrime.securesms.stickers.StickerUrl;
import org.thoughtcrime.securesms.util.AvatarUtil;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.ByteUnit;
import org.signal.core.util.Hex;
import org.thoughtcrime.securesms.util.ImageCompressionUtil;
import org.thoughtcrime.securesms.util.LinkUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.OkHttpUtil;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
@@ -95,7 +96,7 @@ public class LinkPreviewRepository {
CompositeRequestController compositeController = new CompositeRequestController();
if (!LinkPreviewUtil.isValidPreviewUrl(url)) {
if (!LinkUtil.isValidPreviewUrl(url)) {
Log.w(TAG, "Tried to get a link preview for a non-whitelisted domain.");
callback.onError(Error.PREVIEW_NOT_AVAILABLE);
return compositeController;
@@ -164,7 +165,7 @@ public class LinkPreviewRepository {
Optional<String> imageUrl = openGraph.getImageUrl();
long date = openGraph.getDate();
if (imageUrl.isPresent() && !LinkPreviewUtil.isValidPreviewUrl(imageUrl.get())) {
if (imageUrl.isPresent() && !LinkUtil.isValidPreviewUrl(imageUrl.get())) {
Log.i(TAG, "Image URL was invalid or for a non-whitelisted domain. Skipping.");
imageUrl = Optional.empty();
}

View File

@@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.linkpreview;
import android.annotation.SuppressLint;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.URLSpan;
import android.text.util.Linkify;
@@ -14,10 +13,8 @@ import androidx.core.text.util.LinkifyCompat;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.stickers.StickerUrl;
import org.thoughtcrime.securesms.util.DateUtils;
import org.signal.core.util.SetUtil;
import org.thoughtcrime.securesms.util.LinkUtil;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.api.util.OptionalUtil;
@@ -34,12 +31,6 @@ import okhttp3.HttpUrl;
public final class LinkPreviewUtil {
private static final String TAG = Log.tag(LinkPreviewUtil.class);
private static final Pattern DOMAIN_PATTERN = Pattern.compile("^(https?://)?([^/]+).*$");
private static final Pattern ALL_ASCII_PATTERN = Pattern.compile("^[\\x00-\\x7F]*$");
private static final Pattern ALL_NON_ASCII_PATTERN = Pattern.compile("^[^\\x00-\\x7F]*$");
private static final Pattern ILLEGAL_CHARACTERS_PATTERN = Pattern.compile("[\u202C\u202D\u202E\u2500-\u25FF]");
private static final Pattern OPEN_GRAPH_TAG_PATTERN = Pattern.compile("<\\s*meta[^>]*property\\s*=\\s*\"\\s*og:([^\"]+)\"[^>]*/?\\s*>");
private static final Pattern ARTICLE_TAG_PATTERN = Pattern.compile("<\\s*meta[^>]*property\\s*=\\s*\"\\s*article:([^\"]+)\"[^>]*/?\\s*>");
private static final Pattern OPEN_GRAPH_CONTENT_PATTERN = Pattern.compile("content\\s*=\\s*\"([^\"]*)\"");
@@ -47,8 +38,6 @@ public final class LinkPreviewUtil {
private static final Pattern FAVICON_PATTERN = Pattern.compile("<\\s*link[^>]*rel\\s*=\\s*\".*icon.*\"[^>]*>");
private static final Pattern FAVICON_HREF_PATTERN = Pattern.compile("href\\s*=\\s*\"([^\"]*)\"");
private static final Set<String> INVALID_TOP_LEVEL_DOMAINS = SetUtil.newHashSet("onion", "i2p");
public static @Nullable String getTopLevelDomain(@Nullable String urlString) {
if (!Util.isEmpty(urlString)) {
HttpUrl url = HttpUrl.parse(urlString);
@@ -61,7 +50,7 @@ public final class LinkPreviewUtil {
}
/**
* @return All whitelisted URLs in the source text.
* @return All URLs allowed as previews in the source text.
*/
public static @NonNull Links findValidPreviewUrls(@NonNull String text) {
SpannableString spannable = new SpannableString(text);
@@ -73,47 +62,10 @@ public final class LinkPreviewUtil {
return new Links(Stream.of(spannable.getSpans(0, spannable.length(), URLSpan.class))
.map(span -> new Link(span.getURL(), spannable.getSpanStart(span)))
.filter(link -> isValidPreviewUrl(link.getUrl()))
.filter(link -> LinkUtil.isValidPreviewUrl(link.getUrl()))
.toList());
}
/**
* @return True if the host is present in the link whitelist.
*/
public static boolean isValidPreviewUrl(@Nullable String linkUrl) {
if (linkUrl == null) return false;
if (StickerUrl.isValidShareLink(linkUrl)) return true;
HttpUrl url = HttpUrl.parse(linkUrl);
return url != null &&
!TextUtils.isEmpty(url.scheme()) &&
"https".equals(url.scheme()) &&
isLegalUrl(linkUrl);
}
public static boolean isLegalUrl(@NonNull String url) {
if (ILLEGAL_CHARACTERS_PATTERN.matcher(url).find()) {
return false;
}
Matcher matcher = DOMAIN_PATTERN.matcher(url);
if (matcher.matches()) {
String domain = matcher.group(2);
String cleanedDomain = domain.replaceAll("\\.", "");
String topLevelDomain = parseTopLevelDomain(domain);
boolean validCharacters = ALL_ASCII_PATTERN.matcher(cleanedDomain).matches() ||
ALL_NON_ASCII_PATTERN.matcher(cleanedDomain).matches();
boolean validTopLevelDomain = !INVALID_TOP_LEVEL_DOMAINS.contains(topLevelDomain);
return validCharacters && validTopLevelDomain;
} else {
return false;
}
}
public static @NonNull OpenGraph parseOpenGraphFields(@Nullable String html) {
if (html == null) {
return new OpenGraph(Collections.emptyMap(), null, null);
@@ -169,16 +121,6 @@ public final class LinkPreviewUtil {
return new OpenGraph(openGraphTags, htmlTitle, faviconUrl);
}
private static @Nullable String parseTopLevelDomain(@NonNull String domain) {
int periodIndex = domain.lastIndexOf(".");
if (periodIndex >= 0 && periodIndex < domain.length() - 1) {
return domain.substring(periodIndex + 1);
} else {
return null;
}
}
private static @NonNull String fromDoubleEncoded(@NonNull String html) {
return HtmlCompat.fromHtml(HtmlCompat.fromHtml(html, 0).toString(), 0).toString();
}