From ed93b3a4320f0e87386ce3ee657913209f721a2d Mon Sep 17 00:00:00 2001 From: M66B Date: Sat, 13 Aug 2022 19:29:36 +0200 Subject: [PATCH] Open images/favicons unsafe (http, invalid certificate, etc) --- .../eu/faircode/email/ConnectionHelper.java | 76 +++++++++++++++++++ .../java/eu/faircode/email/ContactInfo.java | 32 +------- .../eu/faircode/email/FragmentMessages.java | 2 +- .../email/FragmentOptionsConnection.java | 12 ++- .../main/java/eu/faircode/email/Helper.java | 51 ------------- .../java/eu/faircode/email/ImageHelper.java | 2 +- .../layout/fragment_options_connection.xml | 30 +++++++- app/src/main/res/values/strings.xml | 2 + 8 files changed, 122 insertions(+), 85 deletions(-) diff --git a/app/src/main/java/eu/faircode/email/ConnectionHelper.java b/app/src/main/java/eu/faircode/email/ConnectionHelper.java index 0f26f81b75..5d2287d697 100644 --- a/app/src/main/java/eu/faircode/email/ConnectionHelper.java +++ b/app/src/main/java/eu/faircode/email/ConnectionHelper.java @@ -44,7 +44,10 @@ import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.InterfaceAddress; import java.net.NetworkInterface; +import java.net.URL; +import java.net.URLDecoder; import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; import java.security.cert.Certificate; import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; @@ -57,12 +60,17 @@ import java.util.Locale; import java.util.Objects; import javax.net.SocketFactory; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; import inet.ipaddr.IPAddressString; public class ConnectionHelper { + static final int MAX_REDIRECTS = 5; // https://www.freesoft.org/CIE/RFC/1945/46.htm + static final List PREF_NETWORK = Collections.unmodifiableList(Arrays.asList( "metered", "roaming", "rlah", "require_validated", "vpn_only" // update network state )); @@ -658,4 +666,72 @@ public class ConnectionHelper { } } } + + static HttpURLConnection openConnectionUnsafe(Context context, String source, int ctimeout, int rtimeout) throws IOException { + return openConnectionUnsafe(context, new URL(source), ctimeout, rtimeout); + } + + static HttpURLConnection openConnectionUnsafe(Context context, URL url, int ctimeout, int rtimeout) throws IOException { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + boolean open_unsafe = prefs.getBoolean("open_unsafe", true); + + int redirects = 0; + while (true) { + HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); + urlConnection.setRequestMethod("GET"); + urlConnection.setDoOutput(false); + urlConnection.setReadTimeout(ctimeout); + urlConnection.setConnectTimeout(rtimeout); + urlConnection.setInstanceFollowRedirects(true); + + if (urlConnection instanceof HttpsURLConnection) { + if (open_unsafe) + ((HttpsURLConnection) urlConnection).setHostnameVerifier(new HostnameVerifier() { + @Override + public boolean verify(String hostname, SSLSession session) { + return true; + } + }); + } else { + if (!open_unsafe) + throw new IOException("https required url=" + url); + } + + ConnectionHelper.setUserAgent(context, urlConnection); + urlConnection.connect(); + + try { + int status = urlConnection.getResponseCode(); + + if (open_unsafe && + (status == HttpURLConnection.HTTP_MOVED_PERM || + status == HttpURLConnection.HTTP_MOVED_TEMP || + status == HttpURLConnection.HTTP_SEE_OTHER || + status == 307 /* Temporary redirect */ || + status == 308 /* Permanent redirect */)) { + if (++redirects > MAX_REDIRECTS) + throw new IOException("Too many redirects"); + + String header = urlConnection.getHeaderField("Location"); + if (header == null) + throw new IOException("Location header missing"); + + String location = URLDecoder.decode(header, StandardCharsets.UTF_8.name()); + url = new URL(url, location); + Log.i("Redirect #" + redirects + " to " + url); + + urlConnection.disconnect(); + continue; + } + + if (status != HttpURLConnection.HTTP_OK) + throw new IOException("Error " + status + ": " + urlConnection.getResponseMessage()); + + return urlConnection; + } catch (IOException ex) { + urlConnection.disconnect(); + throw ex; + } + } + } } diff --git a/app/src/main/java/eu/faircode/email/ContactInfo.java b/app/src/main/java/eu/faircode/email/ContactInfo.java index 1f7c6ceaad..2d190a153d 100644 --- a/app/src/main/java/eu/faircode/email/ContactInfo.java +++ b/app/src/main/java/eu/faircode/email/ContactInfo.java @@ -80,13 +80,11 @@ import java.util.concurrent.Future; import javax.mail.Address; import javax.mail.internet.InternetAddress; -import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLException; import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLProtocolException; -import javax.net.ssl.SSLSession; public class ContactInfo { private String email; @@ -552,19 +550,8 @@ public class ContactInfo { boolean favicons_partial = prefs.getBoolean("favicons_partial", true); Log.i("PARSE favicon " + base); - HttpsURLConnection connection = (HttpsURLConnection) base.openConnection(); - connection.setRequestMethod("GET"); - connection.setReadTimeout(FAVICON_READ_TIMEOUT); - connection.setConnectTimeout(FAVICON_CONNECT_TIMEOUT); - connection.setInstanceFollowRedirects(true); - connection.setHostnameVerifier(new HostnameVerifier() { - @Override - public boolean verify(String hostname, SSLSession session) { - return true; - } - }); - ConnectionHelper.setUserAgent(context, connection); - connection.connect(); + HttpURLConnection connection = ConnectionHelper + .openConnectionUnsafe(context, base, FAVICON_CONNECT_TIMEOUT, FAVICON_READ_TIMEOUT); Document doc; try { @@ -840,19 +827,8 @@ public class ContactInfo { if (!"https".equals(url.getProtocol())) throw new FileNotFoundException(url.toString()); - HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); - connection.setRequestMethod("GET"); - connection.setReadTimeout(FAVICON_READ_TIMEOUT); - connection.setConnectTimeout(FAVICON_CONNECT_TIMEOUT); - connection.setInstanceFollowRedirects(true); - connection.setHostnameVerifier(new HostnameVerifier() { - @Override - public boolean verify(String hostname, SSLSession session) { - return true; - } - }); - ConnectionHelper.setUserAgent(context, connection); - connection.connect(); + HttpURLConnection connection = ConnectionHelper + .openConnectionUnsafe(context, url, FAVICON_CONNECT_TIMEOUT, FAVICON_READ_TIMEOUT); try { int status = connection.getResponseCode(); diff --git a/app/src/main/java/eu/faircode/email/FragmentMessages.java b/app/src/main/java/eu/faircode/email/FragmentMessages.java index 9aa5070a0f..bf6f32e741 100644 --- a/app/src/main/java/eu/faircode/email/FragmentMessages.java +++ b/app/src/main/java/eu/faircode/email/FragmentMessages.java @@ -9568,7 +9568,7 @@ public class FragmentMessages extends FragmentBase HttpURLConnection connection = null; try { - connection = Helper.openUrlRedirect(context, src, timeout); + connection = ConnectionHelper.openConnectionUnsafe(context, src, timeout, timeout); Helper.copy(connection.getInputStream(), os); } finally { if (connection != null) diff --git a/app/src/main/java/eu/faircode/email/FragmentOptionsConnection.java b/app/src/main/java/eu/faircode/email/FragmentOptionsConnection.java index 452590bfd6..5f033e2544 100644 --- a/app/src/main/java/eu/faircode/email/FragmentOptionsConnection.java +++ b/app/src/main/java/eu/faircode/email/FragmentOptionsConnection.java @@ -79,6 +79,7 @@ public class FragmentOptionsConnection extends FragmentBase implements SharedPre private SwitchCompat swSslHarden; private SwitchCompat swSslHardenStrict; private SwitchCompat swCertStrict; + private SwitchCompat swOpenUnsafe; private Button btnManage; private TextView tvNetworkMetered; private TextView tvNetworkRoaming; @@ -93,7 +94,7 @@ public class FragmentOptionsConnection extends FragmentBase implements SharedPre "download_headers", "download_eml", "download_plain", "require_validated", "vpn_only", "timeout", "prefer_ip4", "bind_socket", "standalone_vpn", "tcp_keep_alive", - "ssl_harden", "ssl_harden_strict", "cert_strict" + "ssl_harden", "ssl_harden_strict", "cert_strict", "open_unsafe" }; @Override @@ -125,6 +126,7 @@ public class FragmentOptionsConnection extends FragmentBase implements SharedPre swSslHarden = view.findViewById(R.id.swSslHarden); swSslHardenStrict = view.findViewById(R.id.swSslHardenStrict); swCertStrict = view.findViewById(R.id.swCertStrict); + swOpenUnsafe = view.findViewById(R.id.swOpenUnsafe); btnManage = view.findViewById(R.id.btnManage); tvNetworkMetered = view.findViewById(R.id.tvNetworkMetered); @@ -307,6 +309,13 @@ public class FragmentOptionsConnection extends FragmentBase implements SharedPre } }); + swOpenUnsafe.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean checked) { + prefs.edit().putBoolean("open_unsafe", checked).apply(); + } + }); + final Intent manage = getIntentConnectivity(); PackageManager pm = getContext().getPackageManager(); btnManage.setVisibility( @@ -441,6 +450,7 @@ public class FragmentOptionsConnection extends FragmentBase implements SharedPre swSslHardenStrict.setChecked(prefs.getBoolean("ssl_harden_strict", false)); swSslHardenStrict.setEnabled(swSslHarden.isChecked()); swCertStrict.setChecked(prefs.getBoolean("cert_strict", !BuildConfig.PLAY_STORE_RELEASE)); + swOpenUnsafe.setChecked(prefs.getBoolean("open_unsafe", true)); } private static Intent getIntentConnectivity() { diff --git a/app/src/main/java/eu/faircode/email/Helper.java b/app/src/main/java/eu/faircode/email/Helper.java index b47ae617da..98f50475a0 100644 --- a/app/src/main/java/eu/faircode/email/Helper.java +++ b/app/src/main/java/eu/faircode/email/Helper.java @@ -135,9 +135,6 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.lang.reflect.Field; -import java.net.HttpURLConnection; -import java.net.URL; -import java.net.URLDecoder; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; @@ -178,7 +175,6 @@ public class Helper { static final int BUFFER_SIZE = 8192; // Same as in Files class static final long MIN_REQUIRED_SPACE = 250 * 1024L * 1024L; - static final int MAX_REDIRECTS = 5; // https://www.freesoft.org/CIE/RFC/1945/46.htm static final int AUTOLOCK_GRACE = 15; // seconds static final long PIN_FAILURE_DELAY = 3; // seconds @@ -2359,53 +2355,6 @@ public class Helper { //intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Uri.fromFile(initial)); } - static HttpURLConnection openUrlRedirect(Context context, String source, int timeout) throws IOException { - int redirects = 0; - URL url = new URL(source); - while (true) { - HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); - urlConnection.setRequestMethod("GET"); - urlConnection.setDoOutput(false); - urlConnection.setReadTimeout(timeout); - urlConnection.setConnectTimeout(timeout); - urlConnection.setInstanceFollowRedirects(true); - ConnectionHelper.setUserAgent(context, urlConnection); - urlConnection.connect(); - - try { - int status = urlConnection.getResponseCode(); - - if (status == HttpURLConnection.HTTP_MOVED_PERM || - status == HttpURLConnection.HTTP_MOVED_TEMP || - status == HttpURLConnection.HTTP_SEE_OTHER || - status == 307 /* Temporary redirect */ || - status == 308 /* Permanent redirect */) { - if (++redirects > MAX_REDIRECTS) - throw new IOException("Too many redirects"); - - String header = urlConnection.getHeaderField("Location"); - if (header == null) - throw new IOException("Location header missing"); - - String location = URLDecoder.decode(header, StandardCharsets.UTF_8.name()); - url = new URL(url, location); - Log.i("Redirect #" + redirects + " to " + url); - - urlConnection.disconnect(); - continue; - } - - if (status != HttpURLConnection.HTTP_OK) - throw new IOException("Error " + status + ": " + urlConnection.getResponseMessage()); - - return urlConnection; - } catch (IOException ex) { - urlConnection.disconnect(); - throw ex; - } - } - } - static class ByteArrayInOutStream extends ByteArrayOutputStream { public ByteArrayInOutStream() { super(); diff --git a/app/src/main/java/eu/faircode/email/ImageHelper.java b/app/src/main/java/eu/faircode/email/ImageHelper.java index cd3bc185be..72c3aeeba6 100644 --- a/app/src/main/java/eu/faircode/email/ImageHelper.java +++ b/app/src/main/java/eu/faircode/email/ImageHelper.java @@ -692,7 +692,7 @@ class ImageHelper { Bitmap bm; HttpURLConnection urlConnection = null; try { - urlConnection = Helper.openUrlRedirect(context, source, timeout); + urlConnection = ConnectionHelper.openConnectionUnsafe(context, source, timeout, timeout); if (id > 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { File file = getCacheFile(context, id, source, ".blob"); diff --git a/app/src/main/res/layout/fragment_options_connection.xml b/app/src/main/res/layout/fragment_options_connection.xml index cf05fa2cd6..0cd8f9e45f 100644 --- a/app/src/main/res/layout/fragment_options_connection.xml +++ b/app/src/main/res/layout/fragment_options_connection.xml @@ -209,12 +209,12 @@ android:layout_height="wrap_content" android:drawableStart="@drawable/twotone_warning_24" android:drawablePadding="6dp" - app:drawableTint="?attr/colorWarning" android:gravity="center" android:text="@string/title_advanced_advanced" android:textAppearance="@style/TextAppearance.AppCompat.Large" android:textColor="?attr/colorWarning" android:textStyle="bold" + app:drawableTint="?attr/colorWarning" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> @@ -483,6 +483,30 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/swCertStrict" /> + + + +