From 5bdbec77cbe824cecd02b9fdb4dc3d623ec88ce4 Mon Sep 17 00:00:00 2001 From: M66B Date: Thu, 9 Nov 2023 19:06:23 +0100 Subject: [PATCH] Added mini connection checker --- .../eu/faircode/email/ConnectionHelper.java | 102 +++++++++++++ .../java/eu/faircode/email/EmailProvider.java | 106 +------------- .../email/FragmentOptionsConnection.java | 134 ++++++++++++++++++ .../layout/fragment_options_connection.xml | 101 ++++++++++++- 4 files changed, 338 insertions(+), 105 deletions(-) diff --git a/app/src/main/java/eu/faircode/email/ConnectionHelper.java b/app/src/main/java/eu/faircode/email/ConnectionHelper.java index f3569da14b..97ae17813f 100644 --- a/app/src/main/java/eu/faircode/email/ConnectionHelper.java +++ b/app/src/main/java/eu/faircode/email/ConnectionHelper.java @@ -36,7 +36,9 @@ import androidx.preference.PreferenceManager; import com.sun.mail.iap.ConnectionException; import com.sun.mail.util.FolderClosedIOException; +import com.sun.mail.util.LineInputStream; +import java.io.BufferedInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.net.HttpURLConnection; @@ -46,6 +48,8 @@ import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.InterfaceAddress; import java.net.NetworkInterface; +import java.net.Socket; +import java.net.SocketException; import java.net.URL; import java.net.URLDecoder; import java.net.UnknownHostException; @@ -804,4 +808,102 @@ public class ConnectionHelper { return null; } } + + static SSLSocket starttls(Socket socket, String host, int port, Context context) throws IOException { + String response; + String command; + boolean has = false; + + LineInputStream lis = + new LineInputStream( + new BufferedInputStream( + socket.getInputStream())); + + if (port == 587) { + do { + response = lis.readLine(); + if (response != null) + EntityLog.log(context, EntityLog.Type.Protocol, + socket.getRemoteSocketAddress() + " <" + response); + } while (response != null && !response.startsWith("220 ")); + + command = "EHLO " + EmailService.getDefaultEhlo() + "\n"; + EntityLog.log(context, socket.getRemoteSocketAddress() + " >" + command); + socket.getOutputStream().write(command.getBytes()); + + do { + response = lis.readLine(); + if (response != null) { + EntityLog.log(context, EntityLog.Type.Protocol, + socket.getRemoteSocketAddress() + " <" + response); + if (response.contains("STARTTLS")) + has = true; + } + } while (response != null && + response.length() >= 4 && response.charAt(3) == '-'); + + if (has) { + command = "STARTTLS\n"; + EntityLog.log(context, EntityLog.Type.Protocol, + socket.getRemoteSocketAddress() + " >" + command); + socket.getOutputStream().write(command.getBytes()); + } + } else if (port == 143) { + do { + response = lis.readLine(); + if (response != null) { + EntityLog.log(context, EntityLog.Type.Protocol, + socket.getRemoteSocketAddress() + " <" + response); + if (response.contains("STARTTLS")) + has = true; + } + } while (response != null && + !response.startsWith("* OK")); + + if (has) { + command = "A001 STARTTLS\n"; + EntityLog.log(context, EntityLog.Type.Protocol, + socket.getRemoteSocketAddress() + " >" + command); + socket.getOutputStream().write(command.getBytes()); + } + } + + if (has) { + do { + response = lis.readLine(); + if (response != null) + EntityLog.log(context, EntityLog.Type.Protocol, + socket.getRemoteSocketAddress() + " <" + response); + } while (response != null && + !(response.startsWith("A001 OK") || response.startsWith("220 "))); + + SSLSocketFactory sslFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); + return (SSLSocket) sslFactory.createSocket(socket, host, port, false); + } else + throw new SocketException("No STARTTLS"); + } + + static void signOff(Socket socket, int port, Context context) { + try { + String command = (port == 465 || port == 587 ? "QUIT" : "A002 LOGOUT"); + + EntityLog.log(context, EntityLog.Type.Protocol, + socket.getRemoteSocketAddress() + " >" + command); + socket.getOutputStream().write((command + "\n").getBytes()); + + LineInputStream lis = + new LineInputStream( + new BufferedInputStream( + socket.getInputStream())); + String response; + do { + response = lis.readLine(); + if (response != null) + EntityLog.log(context, EntityLog.Type.Protocol, + socket.getRemoteSocketAddress() + " <" + response); + } while (response != null); + } catch (IOException ex) { + Log.w(ex); + } + } } diff --git a/app/src/main/java/eu/faircode/email/EmailProvider.java b/app/src/main/java/eu/faircode/email/EmailProvider.java index bdeddd9e8d..d97764215e 100644 --- a/app/src/main/java/eu/faircode/email/EmailProvider.java +++ b/app/src/main/java/eu/faircode/email/EmailProvider.java @@ -30,13 +30,10 @@ import android.util.Xml; import androidx.annotation.NonNull; -import com.sun.mail.util.LineInputStream; - import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; -import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -50,7 +47,6 @@ import java.net.HttpURLConnection; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; -import java.net.SocketException; import java.net.URL; import java.net.UnknownHostException; import java.security.cert.Certificate; @@ -1360,7 +1356,7 @@ public class EmailProvider implements Parcelable { SSLSocket sslSocket = null; try { if (starttls) - sslSocket = starttls(socket, context); + sslSocket = ConnectionHelper.starttls(socket, host, port, context); else sslSocket = (SSLSocket) socket; @@ -1405,7 +1401,7 @@ public class EmailProvider implements Parcelable { } finally { try { if (sslSocket != null) { - signOff(sslSocket, context); + ConnectionHelper.signOff(sslSocket, port, context); sslSocket.close(); } } catch (Throwable ex) { @@ -1440,104 +1436,6 @@ public class EmailProvider implements Parcelable { }); } - private SSLSocket starttls(Socket socket, Context context) throws IOException { - String response; - String command; - boolean has = false; - - LineInputStream lis = - new LineInputStream( - new BufferedInputStream( - socket.getInputStream())); - - if (port == 587) { - do { - response = lis.readLine(); - if (response != null) - EntityLog.log(context, EntityLog.Type.Protocol, - socket.getRemoteSocketAddress() + " <" + response); - } while (response != null && !response.startsWith("220 ")); - - command = "EHLO " + EmailService.getDefaultEhlo() + "\n"; - EntityLog.log(context, socket.getRemoteSocketAddress() + " >" + command); - socket.getOutputStream().write(command.getBytes()); - - do { - response = lis.readLine(); - if (response != null) { - EntityLog.log(context, EntityLog.Type.Protocol, - socket.getRemoteSocketAddress() + " <" + response); - if (response.contains("STARTTLS")) - has = true; - } - } while (response != null && - response.length() >= 4 && response.charAt(3) == '-'); - - if (has) { - command = "STARTTLS\n"; - EntityLog.log(context, EntityLog.Type.Protocol, - socket.getRemoteSocketAddress() + " >" + command); - socket.getOutputStream().write(command.getBytes()); - } - } else if (port == 143) { - do { - response = lis.readLine(); - if (response != null) { - EntityLog.log(context, EntityLog.Type.Protocol, - socket.getRemoteSocketAddress() + " <" + response); - if (response.contains("STARTTLS")) - has = true; - } - } while (response != null && - !response.startsWith("* OK")); - - if (has) { - command = "A001 STARTTLS\n"; - EntityLog.log(context, EntityLog.Type.Protocol, - socket.getRemoteSocketAddress() + " >" + command); - socket.getOutputStream().write(command.getBytes()); - } - } - - if (has) { - do { - response = lis.readLine(); - if (response != null) - EntityLog.log(context, EntityLog.Type.Protocol, - socket.getRemoteSocketAddress() + " <" + response); - } while (response != null && - !(response.startsWith("A001 OK") || response.startsWith("220 "))); - - SSLSocketFactory sslFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); - return (SSLSocket) sslFactory.createSocket(socket, host, port, false); - } else - throw new SocketException("No STARTTLS"); - } - - private void signOff(Socket socket, Context context) { - try { - String command = (port == 465 || port == 587 ? "QUIT" : "A002 LOGOUT"); - - EntityLog.log(context, EntityLog.Type.Protocol, - socket.getRemoteSocketAddress() + " >" + command); - socket.getOutputStream().write((command + "\n").getBytes()); - - LineInputStream lis = - new LineInputStream( - new BufferedInputStream( - socket.getInputStream())); - String response; - do { - response = lis.readLine(); - if (response != null) - EntityLog.log(context, EntityLog.Type.Protocol, - socket.getRemoteSocketAddress() + " <" + response); - } while (response != null); - } catch (IOException ex) { - Log.w(ex); - } - } - @Override public boolean equals(Object obj) { if (obj instanceof Server) { diff --git a/app/src/main/java/eu/faircode/email/FragmentOptionsConnection.java b/app/src/main/java/eu/faircode/email/FragmentOptionsConnection.java index dc03d62c66..db38896cb9 100644 --- a/app/src/main/java/eu/faircode/email/FragmentOptionsConnection.java +++ b/app/src/main/java/eu/faircode/email/FragmentOptionsConnection.java @@ -46,6 +46,7 @@ import android.widget.Button; import android.widget.CompoundButton; import android.widget.EditText; import android.widget.ImageButton; +import android.widget.RadioGroup; import android.widget.Spinner; import android.widget.TextView; @@ -58,6 +59,16 @@ import androidx.constraintlayout.widget.Group; import androidx.lifecycle.Lifecycle; import androidx.preference.PreferenceManager; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; + +import javax.net.SocketFactory; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + public class FragmentOptionsConnection extends FragmentBase implements SharedPreferences.OnSharedPreferenceChangeListener { private View view; private ImageButton ibHelp; @@ -86,6 +97,10 @@ public class FragmentOptionsConnection extends FragmentBase implements SharedPre private TextView tvNetworkRoaming; private CardView cardDebug; private Button btnCiphers; + private EditText etHost; + private RadioGroup rgEncryption; + private EditText etPort; + private Button btnCheck; private TextView tvNetworkInfo; private Group grpValidated; @@ -136,6 +151,10 @@ public class FragmentOptionsConnection extends FragmentBase implements SharedPre cardDebug = view.findViewById(R.id.cardDebug); btnCiphers = view.findViewById(R.id.btnCiphers); + etHost = view.findViewById(R.id.etHost); + rgEncryption = view.findViewById(R.id.rgEncryption); + etPort = view.findViewById(R.id.etPort); + btnCheck = view.findViewById(R.id.btnCheck); tvNetworkInfo = view.findViewById(R.id.tvNetworkInfo); grpValidated = view.findViewById(R.id.grpValidated); @@ -359,6 +378,121 @@ public class FragmentOptionsConnection extends FragmentBase implements SharedPre } }); + btnCheck.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + String host = etHost.getText().toString().trim(); + Integer port = Helper.parseInt(etPort.getText().toString().trim()); + + String encryption; + if (rgEncryption.getCheckedRadioButtonId() == R.id.radio_starttls) + encryption = "starttls"; + else if (rgEncryption.getCheckedRadioButtonId() == R.id.radio_ssl) + encryption = "ssl"; + else + encryption = "none"; + + int timeout = prefs.getInt("timeout", EmailService.DEFAULT_CONNECT_TIMEOUT) * 1000; + + Bundle args = new Bundle(); + args.putString("host", host); + args.putInt("port", port == null ? 0 : port); + args.putString("encryption", encryption); + args.putInt("timeout", timeout); + + new SimpleTask() { + @Override + protected void onPreExecute(Bundle args) { + btnCheck.setEnabled(false); + } + + @Override + protected void onPostExecute(Bundle args) { + btnCheck.setEnabled(true); + } + + @Override + protected StringBuilder onExecute(Context context, Bundle args) throws Throwable { + String host = args.getString("host"); + int port = args.getInt("port"); + String encryption = args.getString("encryption"); + int timeout = args.getInt("timeout"); + + StringBuilder sb = new StringBuilder(); + sb.append("Host: ").append(host).append('\n'); + sb.append("Port: ").append(port).append('\n'); + sb.append("Encryption: ").append(encryption).append('\n'); + + InetSocketAddress address = new InetSocketAddress(host, port); + SocketFactory factory = (!"ssl".equals(encryption) + ? SocketFactory.getDefault() + : SSLSocketFactory.getDefault()); + try (Socket socket = factory.createSocket()) { + socket.connect(address, timeout); + socket.setSoTimeout(timeout); + + if (!"none".equals(encryption)) { + SSLSocket sslSocket = null; + try { + if ("starttls".equals(encryption)) + sslSocket = ConnectionHelper.starttls(socket, host, port, context); + else + sslSocket = (SSLSocket) socket; + + sslSocket.startHandshake(); + + SSLSession session = sslSocket.getSession(); + sb.append("Protocol: ").append(session.getProtocol()).append('\n'); + sb.append("Cipher: ").append(session.getCipherSuite()).append('\n'); + Certificate[] certificates = session.getPeerCertificates(); + if (certificates != null) + for (Certificate certificate : certificates) { + if (certificate instanceof X509Certificate) { + X509Certificate x = (X509Certificate) certificate; + sb.append("Subject: ").append(x.getSubjectDN()).append('\n'); + for (String dns : EntityCertificate.getDnsNames(x)) + sb.append("DNS name: ").append(dns).append('\n'); + } + } + } finally { + try { + if (sslSocket != null) { + ConnectionHelper.signOff(sslSocket, port, context); + sslSocket.close(); + } + } catch (Throwable ex) { + Log.e(ex); + } + } + } + } + + return sb; + } + + @Override + protected void onExecuted(Bundle args, StringBuilder sb) { + new AlertDialog.Builder(getContext()) + .setIcon(R.drawable.twotone_info_24) + .setTitle(R.string.title_advanced_section_connection) + .setMessage(sb) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // Do nothing + } + }) + .show(); + } + + @Override + protected void onException(Bundle args, Throwable ex) { + Log.unexpectedError(getParentFragment(), ex); + } + }.execute(FragmentOptionsConnection.this, args, "connection:check"); + } + }); + // Initialize FragmentDialogTheme.setBackground(getContext(), view, false); tvNetworkMetered.setVisibility(View.GONE); diff --git a/app/src/main/res/layout/fragment_options_connection.xml b/app/src/main/res/layout/fragment_options_connection.xml index bf257079fa..20057cef22 100644 --- a/app/src/main/res/layout/fragment_options_connection.xml +++ b/app/src/main/res/layout/fragment_options_connection.xml @@ -611,6 +611,105 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/tvCaptionDebug" /> + + + + + + + + + + + + + + + + + + + + + + + +