Migrate to AndroidX from the Android Support Library. https://redmine.stoutner.com...
[PrivacyBrowser.git] / app / src / main / java / com / stoutner / privacybrowser / dialogs / SslCertificateErrorDialog.java
1 /*
2  * Copyright © 2016-2019 Soren Stoutner <soren@stoutner.com>.
3  *
4  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
5  *
6  * Privacy Browser is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * Privacy Browser is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with Privacy Browser.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 package com.stoutner.privacybrowser.dialogs;
21
22 import android.annotation.SuppressLint;
23 import android.app.Activity;
24 import android.app.AlertDialog;
25 import android.app.Dialog;
26 import android.content.Context;
27 import android.content.DialogInterface;
28 import android.net.Uri;
29 import android.net.http.SslCertificate;
30 import android.net.http.SslError;
31 import android.os.AsyncTask;
32 import android.os.Bundle;
33 import android.text.SpannableStringBuilder;
34 import android.text.Spanned;
35 import android.text.style.ForegroundColorSpan;
36 import android.view.LayoutInflater;
37 import android.view.WindowManager;
38 import android.widget.TextView;
39
40 import androidx.annotation.NonNull;
41 import androidx.fragment.app.DialogFragment;  // The AndroidX dialog fragment must be used or an error is produced on API <=22.
42
43 import com.stoutner.privacybrowser.R;
44 import com.stoutner.privacybrowser.activities.MainWebViewActivity;
45
46 import java.lang.ref.WeakReference;
47 import java.net.InetAddress;
48 import java.net.UnknownHostException;
49 import java.text.DateFormat;
50 import java.util.Date;
51
52 public class SslCertificateErrorDialog extends DialogFragment {
53     // `sslCertificateErrorListener` is used in `onAttach` and `onCreateDialog`.
54     private SslCertificateErrorListener sslCertificateErrorListener;
55
56     // The public interface is used to send information back to the parent activity.
57     public interface SslCertificateErrorListener {
58         void onSslErrorCancel();
59
60         void onSslErrorProceed();
61     }
62
63     public void onAttach(Context context) {
64         // Run the default commands.
65         super.onAttach(context);
66
67         // Get a handle for `SslCertificateErrorListener` from the launching context.
68         sslCertificateErrorListener = (SslCertificateErrorListener) context;
69     }
70
71     public static SslCertificateErrorDialog displayDialog(SslError error) {
72         // Get the various components of the SSL error message.
73         int primaryErrorIntForBundle = error.getPrimaryError();
74         String urlWithErrorForBundle = error.getUrl();
75         SslCertificate sslCertificate = error.getCertificate();
76         String issuedToCNameForBundle = sslCertificate.getIssuedTo().getCName();
77         String issuedToONameForBundle = sslCertificate.getIssuedTo().getOName();
78         String issuedToUNameForBundle = sslCertificate.getIssuedTo().getUName();
79         String issuedByCNameForBundle = sslCertificate.getIssuedBy().getCName();
80         String issuedByONameForBundle = sslCertificate.getIssuedBy().getOName();
81         String issuedByUNameForBundle = sslCertificate.getIssuedBy().getUName();
82         Date startDateForBundle = sslCertificate.getValidNotBeforeDate();
83         Date endDateForBundle = sslCertificate.getValidNotAfterDate();
84
85         // Store the SSL error message components in a `Bundle`.
86         Bundle argumentsBundle = new Bundle();
87         argumentsBundle.putInt("PrimaryErrorInt", primaryErrorIntForBundle);
88         argumentsBundle.putString("UrlWithError", urlWithErrorForBundle);
89         argumentsBundle.putString("IssuedToCName", issuedToCNameForBundle);
90         argumentsBundle.putString("IssuedToOName", issuedToONameForBundle);
91         argumentsBundle.putString("IssuedToUName", issuedToUNameForBundle);
92         argumentsBundle.putString("IssuedByCName", issuedByCNameForBundle);
93         argumentsBundle.putString("IssuedByOName", issuedByONameForBundle);
94         argumentsBundle.putString("IssuedByUName", issuedByUNameForBundle);
95         argumentsBundle.putString("StartDate", DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(startDateForBundle));
96         argumentsBundle.putString("EndDate", DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(endDateForBundle));
97
98         // Add `argumentsBundle` to this instance of `SslCertificateErrorDialog`.
99         SslCertificateErrorDialog thisSslCertificateErrorDialog = new SslCertificateErrorDialog();
100         thisSslCertificateErrorDialog.setArguments(argumentsBundle);
101         return thisSslCertificateErrorDialog;
102     }
103
104     // `@SuppressLing("InflateParams")` removes the warning about using `null` as the parent view group when inflating the `AlertDialog`.
105     @SuppressLint("InflateParams")
106     @SuppressWarnings("deprecation")
107     @Override
108     @NonNull
109     public Dialog onCreateDialog(Bundle savedInstanceState) {
110         // Remove the incorrect lint warning that `getArguments()` might be null.
111         assert getArguments() != null;
112
113         // Get the components of the SSL error message from the bundle.
114         int primaryErrorInt = getArguments().getInt("PrimaryErrorInt");
115         String urlWithErrors = getArguments().getString("UrlWithError");
116         String issuedToCName = getArguments().getString("IssuedToCName");
117         String issuedToOName = getArguments().getString("IssuedToOName");
118         String issuedToUName = getArguments().getString("IssuedToUName");
119         String issuedByCName = getArguments().getString("IssuedByCName");
120         String issuedByOName = getArguments().getString("IssuedByOName");
121         String issuedByUName = getArguments().getString("IssuedByUName");
122         String startDate = getArguments().getString("StartDate");
123         String endDate = getArguments().getString("EndDate");
124
125         // Remove the incorrect lint warning that `getActivity()` might be null.
126         assert getActivity() != null;
127
128         // Get the activity's layout inflater.
129         LayoutInflater layoutInflater = getActivity().getLayoutInflater();
130
131         // Use an alert dialog builder to create the alert dialog.
132         AlertDialog.Builder dialogBuilder;
133
134         // Set the style and icon according to the theme.
135         if (MainWebViewActivity.darkTheme) {
136             // Set the style.
137             dialogBuilder = new AlertDialog.Builder(getActivity(), R.style.PrivacyBrowserAlertDialogDark);
138
139             // Set the icon.
140             dialogBuilder.setIcon(R.drawable.ssl_certificate_enabled_dark);
141         } else {
142             // Set the style.
143             dialogBuilder = new AlertDialog.Builder(getActivity(), R.style.PrivacyBrowserAlertDialogLight);
144
145             // Set the icon.
146             dialogBuilder.setIcon(R.drawable.ssl_certificate_enabled_light);
147         }
148
149         // Set the title.
150         dialogBuilder.setTitle(R.string.ssl_certificate_error);
151
152         // Set the view.  The parent view is `null` because it will be assigned by `AlertDialog`.
153         dialogBuilder.setView(layoutInflater.inflate(R.layout.ssl_certificate_error, null));
154
155         // Set a listener on the negative button.
156         dialogBuilder.setNegativeButton(R.string.cancel, (DialogInterface dialog, int which) -> sslCertificateErrorListener.onSslErrorCancel());
157
158         // Set a listener on the positive button.
159         dialogBuilder.setPositiveButton(R.string.proceed, (DialogInterface dialog, int which) -> sslCertificateErrorListener.onSslErrorProceed());
160
161
162         // Create an alert dialog from the alert dialog builder.
163         AlertDialog alertDialog = dialogBuilder.create();
164
165         // Disable screenshots if not allowed.
166         if (!MainWebViewActivity.allowScreenshots) {
167             // Remove the warning below that `getWindow()` might be null.
168             assert alertDialog.getWindow() != null;
169
170             // Disable screenshots.
171             alertDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
172         }
173
174         // Get a URI for the URL with errors.
175         Uri uriWithErrors = Uri.parse(urlWithErrors);
176
177         // Get the IP addresses for the URI.
178         new GetIpAddresses(getActivity(), alertDialog).execute(uriWithErrors.getHost());
179
180         // The alert dialog must be shown before the contents can be modified.
181         alertDialog.show();
182
183         // Get handles for the `TextViews`
184         TextView primaryErrorTextView = alertDialog.findViewById(R.id.primary_error);
185         TextView urlTextView = alertDialog.findViewById(R.id.url);
186         TextView issuedToCNameTextView = alertDialog.findViewById(R.id.issued_to_cname);
187         TextView issuedToONameTextView = alertDialog.findViewById(R.id.issued_to_oname);
188         TextView issuedToUNameTextView = alertDialog.findViewById(R.id.issued_to_uname);
189         TextView issuedByTextView = alertDialog.findViewById(R.id.issued_by_textview);
190         TextView issuedByCNameTextView = alertDialog.findViewById(R.id.issued_by_cname);
191         TextView issuedByONameTextView = alertDialog.findViewById(R.id.issued_by_oname);
192         TextView issuedByUNameTextView = alertDialog.findViewById(R.id.issued_by_uname);
193         TextView validDatesTextView = alertDialog.findViewById(R.id.valid_dates_textview);
194         TextView startDateTextView = alertDialog.findViewById(R.id.start_date);
195         TextView endDateTextView = alertDialog.findViewById(R.id.end_date);
196
197         // Setup the common strings.
198         String urlLabel = getString(R.string.url_label) + "  ";
199         String cNameLabel = getString(R.string.common_name) + "  ";
200         String oNameLabel = getString(R.string.organization) + "  ";
201         String uNameLabel = getString(R.string.organizational_unit) + "  ";
202         String startDateLabel = getString(R.string.start_date) + "  ";
203         String endDateLabel = getString(R.string.end_date) + "  ";
204
205         // Create a spannable string builder for each text view that needs multiple colors of text.
206         SpannableStringBuilder urlStringBuilder = new SpannableStringBuilder(urlLabel + urlWithErrors);
207         SpannableStringBuilder issuedToCNameStringBuilder = new SpannableStringBuilder(cNameLabel + issuedToCName);
208         SpannableStringBuilder issuedToONameStringBuilder = new SpannableStringBuilder(oNameLabel + issuedToOName);
209         SpannableStringBuilder issuedToUNameStringBuilder = new SpannableStringBuilder(uNameLabel + issuedToUName);
210         SpannableStringBuilder issuedByCNameStringBuilder = new SpannableStringBuilder(cNameLabel + issuedByCName);
211         SpannableStringBuilder issuedByONameStringBuilder = new SpannableStringBuilder(oNameLabel + issuedByOName);
212         SpannableStringBuilder issuedByUNameStringBuilder = new SpannableStringBuilder(uNameLabel + issuedByUName);
213         SpannableStringBuilder startDateStringBuilder = new SpannableStringBuilder(startDateLabel + startDate);
214         SpannableStringBuilder endDateStringBuilder = new SpannableStringBuilder((endDateLabel + endDate));
215
216         // Create a red foreground color span.  The deprecated `getResources().getColor` must be used until the minimum API >= 23.
217         @SuppressWarnings("deprecation") ForegroundColorSpan redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_a700));
218
219         // Create a blue `ForegroundColorSpan`.
220         ForegroundColorSpan blueColorSpan;
221
222         // Set a blue color span according to the theme.  The deprecated `getResources().getColor` must be used until the minimum API >= 23.
223         if (MainWebViewActivity.darkTheme) {
224             //noinspection deprecation
225             blueColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.blue_400));
226         } else {
227             //noinspection deprecation
228             blueColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.blue_700));
229         }
230
231         // Setup the spans to display the certificate information in blue.  `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
232         urlStringBuilder.setSpan(blueColorSpan, urlLabel.length(), urlStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
233         issuedToCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length(), issuedToCNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
234         issuedToONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length(), issuedToONameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
235         issuedToUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length(), issuedToUNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
236         issuedByCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length(), issuedByCNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
237         issuedByONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length(), issuedByONameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
238         issuedByUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length(), issuedByUNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
239         startDateStringBuilder.setSpan(blueColorSpan, startDateLabel.length(), startDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
240         endDateStringBuilder.setSpan(blueColorSpan, endDateLabel.length(), endDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
241
242         // Initialize `primaryErrorString`.
243         String primaryErrorString = "";
244
245         // Highlight the primary error in red and store the primary error string in `primaryErrorString`.
246         switch (primaryErrorInt) {
247             case SslError.SSL_IDMISMATCH:
248                 // Change the URL span colors to red.
249                 urlStringBuilder.setSpan(redColorSpan, urlLabel.length(), urlStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
250                 issuedToCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length(), issuedToCNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
251
252                 // Store the primary error string.
253                 primaryErrorString = getString(R.string.cn_mismatch);
254                 break;
255
256             case SslError.SSL_UNTRUSTED:
257                 // Change the issued by text view text to red.  The deprecated `getResources().getColor` must be used until the minimum API >= 23.
258                 issuedByTextView.setTextColor(getResources().getColor(R.color.red_a700));
259
260                 // Change the issued by span color to red.
261                 issuedByCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length(), issuedByCNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
262                 issuedByONameStringBuilder.setSpan(redColorSpan, oNameLabel.length(), issuedByONameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
263                 issuedByUNameStringBuilder.setSpan(redColorSpan, uNameLabel.length(), issuedByUNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
264
265                 // Store the primary error string.
266                 primaryErrorString = getString(R.string.untrusted);
267                 break;
268
269             case SslError.SSL_DATE_INVALID:
270                 // Change the valid dates text view text to red.  The deprecated `getResources().getColor` must be used until the minimum API >= 23.
271                 validDatesTextView.setTextColor(getResources().getColor(R.color.red_a700));
272
273                 // Change the date span colors to red.
274                 startDateStringBuilder.setSpan(redColorSpan, startDateLabel.length(), startDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
275                 endDateStringBuilder.setSpan(redColorSpan, endDateLabel.length(), endDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
276
277                 // Store the primary error string.
278                 primaryErrorString = getString(R.string.invalid_date);
279                 break;
280
281             case SslError.SSL_NOTYETVALID:
282                 // Change the start date span color to red.
283                 startDateStringBuilder.setSpan(redColorSpan, startDateLabel.length(), startDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
284
285                 // Store the primary error string.
286                 primaryErrorString = getString(R.string.future_certificate);
287                 break;
288
289             case SslError.SSL_EXPIRED:
290                 // Change the end date span color to red.
291                 endDateStringBuilder.setSpan(redColorSpan, endDateLabel.length(), endDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
292
293                 // Store the primary error string.
294                 primaryErrorString = getString(R.string.expired_certificate);
295                 break;
296
297             case SslError.SSL_INVALID:
298                 // Store the primary error string.
299                 primaryErrorString = getString(R.string.invalid_certificate);
300                 break;
301         }
302
303
304         // Display the strings.
305         primaryErrorTextView.setText(primaryErrorString);
306         urlTextView.setText(urlStringBuilder);
307         issuedToCNameTextView.setText(issuedToCNameStringBuilder);
308         issuedToONameTextView.setText(issuedToONameStringBuilder);
309         issuedToUNameTextView.setText(issuedToUNameStringBuilder);
310         issuedByCNameTextView.setText(issuedByCNameStringBuilder);
311         issuedByONameTextView.setText(issuedByONameStringBuilder);
312         issuedByUNameTextView.setText(issuedByUNameStringBuilder);
313         startDateTextView.setText(startDateStringBuilder);
314         endDateTextView.setText(endDateStringBuilder);
315
316         // `onCreateDialog` requires the return of an alert dialog.
317         return alertDialog;
318     }
319
320
321     // This must run asynchronously because it involves a network request.  `String` declares the parameters.  `Void` does not declare progress units.  `SpannableStringBuilder` contains the results.
322     private static class GetIpAddresses extends AsyncTask<String, Void, SpannableStringBuilder> {
323         // The weak references are used to determine if the activity or the alert dialog have disappeared while the AsyncTask is running.
324         private WeakReference<Activity> activityWeakReference;
325         private WeakReference<AlertDialog> alertDialogWeakReference;
326
327         GetIpAddresses(Activity activity, AlertDialog alertDialog) {
328             // Populate the weak references.
329             activityWeakReference = new WeakReference<>(activity);
330             alertDialogWeakReference = new WeakReference<>(alertDialog);
331         }
332
333         @Override
334         protected SpannableStringBuilder doInBackground(String... domainName) {
335             // Get handles for the activity and the alert dialog.
336             Activity activity = activityWeakReference.get();
337             AlertDialog alertDialog = alertDialogWeakReference.get();
338
339             // Abort if the activity or the dialog is gone.
340             if ((activity == null) || (activity.isFinishing()) || (alertDialog == null)) {
341                 return new SpannableStringBuilder();
342             }
343
344             // Initialize an IP address string builder.
345             StringBuilder ipAddresses = new StringBuilder();
346
347             // Get an array with the IP addresses for the host.
348             try {
349                 // Get an array with all the IP addresses for the domain.
350                 InetAddress[] inetAddressesArray = InetAddress.getAllByName(domainName[0]);
351
352                 // Add each IP address to the string builder.
353                 for (InetAddress inetAddress : inetAddressesArray) {
354                     if (ipAddresses.length() == 0) {  // This is the first IP address.
355                         // Add the IP Address to the string builder.
356                         ipAddresses.append(inetAddress.getHostAddress());
357                     } else {  // This is not the first IP address.
358                         // Add a line break to the string builder first.
359                         ipAddresses.append("\n");
360
361                         // Add the IP address to the string builder.
362                         ipAddresses.append(inetAddress.getHostAddress());
363                     }
364                 }
365             } catch (UnknownHostException exception) {
366                 // Do nothing.
367             }
368
369             // Set the label.
370             String ipAddressesLabel = activity.getString(R.string.ip_addresses) + "  ";
371
372             // Create a spannable string builder.
373             SpannableStringBuilder ipAddressesStringBuilder = new SpannableStringBuilder(ipAddressesLabel + ipAddresses);
374
375             // Create a blue foreground color span.
376             ForegroundColorSpan blueColorSpan;
377
378             // Set the blue color span according to the theme.  The deprecated `getColor()` must be used until the minimum API >= 23.
379             if (MainWebViewActivity.darkTheme) {
380                 //noinspection deprecation
381                 blueColorSpan = new ForegroundColorSpan(activity.getResources().getColor(R.color.blue_400));
382             } else {
383                 //noinspection deprecation
384                 blueColorSpan = new ForegroundColorSpan(activity.getResources().getColor(R.color.blue_700));
385             }
386
387             // Set the string builder to display the certificate information in blue.  `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
388             ipAddressesStringBuilder.setSpan(blueColorSpan, ipAddressesLabel.length(), ipAddressesStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
389
390             // Return the formatted string.
391             return ipAddressesStringBuilder;
392         }
393
394         // `onPostExecute()` operates on the UI thread.
395         @Override
396         protected void onPostExecute(SpannableStringBuilder ipAddresses) {
397             // Get handles for the activity and the alert dialog.
398             Activity activity = activityWeakReference.get();
399             AlertDialog alertDialog = alertDialogWeakReference.get();
400
401             // Abort if the activity or the alert dialog is gone.
402             if ((activity == null) || (activity.isFinishing()) || (alertDialog == null)) {
403                 return;
404             }
405
406             // Get a handle for the IP addresses text view.
407             TextView ipAddressesTextView = alertDialog.findViewById(R.id.ip_addresses);
408
409             // Populate the IP addresses text view.
410             ipAddressesTextView.setText(ipAddresses);
411         }
412     }
413 }