Make SSL errors tab aware.
[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.DialogInterface;
27 import android.content.SharedPreferences;
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.preference.PreferenceManager;
34 import android.text.SpannableStringBuilder;
35 import android.text.Spanned;
36 import android.text.style.ForegroundColorSpan;
37 import android.view.LayoutInflater;
38 import android.view.View;
39 import android.view.WindowManager;
40 import android.webkit.SslErrorHandler;
41 import android.widget.TextView;
42
43 import androidx.annotation.NonNull;
44 import androidx.fragment.app.DialogFragment;  // The AndroidX dialog fragment must be used or an error is produced on API <=22.
45
46 import com.stoutner.privacybrowser.R;
47 import com.stoutner.privacybrowser.activities.MainWebViewActivity;
48 import com.stoutner.privacybrowser.fragments.WebViewTabFragment;
49 import com.stoutner.privacybrowser.views.NestedScrollWebView;
50
51 import java.lang.ref.WeakReference;
52 import java.net.InetAddress;
53 import java.net.UnknownHostException;
54 import java.text.DateFormat;
55 import java.util.Date;
56
57 public class SslCertificateErrorDialog extends DialogFragment {
58     public static SslCertificateErrorDialog displayDialog(SslError error, long webViewFragmentId) {
59         // Get the various components of the SSL error message.
60         int primaryErrorIntForBundle = error.getPrimaryError();
61         String urlWithErrorForBundle = error.getUrl();
62         SslCertificate sslCertificate = error.getCertificate();
63         String issuedToCNameForBundle = sslCertificate.getIssuedTo().getCName();
64         String issuedToONameForBundle = sslCertificate.getIssuedTo().getOName();
65         String issuedToUNameForBundle = sslCertificate.getIssuedTo().getUName();
66         String issuedByCNameForBundle = sslCertificate.getIssuedBy().getCName();
67         String issuedByONameForBundle = sslCertificate.getIssuedBy().getOName();
68         String issuedByUNameForBundle = sslCertificate.getIssuedBy().getUName();
69         Date startDateForBundle = sslCertificate.getValidNotBeforeDate();
70         Date endDateForBundle = sslCertificate.getValidNotAfterDate();
71
72         // Create an arguments bundle.
73         Bundle argumentsBundle = new Bundle();
74
75         // Store the SSL error message components in a `Bundle`.
76         argumentsBundle.putInt("primary_error_int", primaryErrorIntForBundle);
77         argumentsBundle.putString("url_with_error", urlWithErrorForBundle);
78         argumentsBundle.putString("issued_to_cname", issuedToCNameForBundle);
79         argumentsBundle.putString("issued_to_oname", issuedToONameForBundle);
80         argumentsBundle.putString("issued_to_uname", issuedToUNameForBundle);
81         argumentsBundle.putString("issued_by_cname", issuedByCNameForBundle);
82         argumentsBundle.putString("issued_by_oname", issuedByONameForBundle);
83         argumentsBundle.putString("issued_by_uname", issuedByUNameForBundle);
84         argumentsBundle.putString("start_date", DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(startDateForBundle));
85         argumentsBundle.putString("end_date", DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(endDateForBundle));
86         argumentsBundle.putLong("webview_fragment_id", webViewFragmentId);
87
88         // Create a new instance of the SSL certificate error dialog.
89         SslCertificateErrorDialog thisSslCertificateErrorDialog = new SslCertificateErrorDialog();
90
91         // Add the arguments bundle to the new dialog.
92         thisSslCertificateErrorDialog.setArguments(argumentsBundle);
93
94         // Return the new dialog.
95         return thisSslCertificateErrorDialog;
96     }
97
98     // `@SuppressLing("InflateParams")` removes the warning about using `null` as the parent view group when inflating the `AlertDialog`.
99     @SuppressLint("InflateParams")
100     @Override
101     @NonNull
102     public Dialog onCreateDialog(Bundle savedInstanceState) {
103         // Get a handle for the arguments.
104         Bundle arguments = getArguments();
105
106         // Remove the incorrect lint warning that the arguments might be null.
107         assert arguments != null;
108
109         // Get the variables from the bundle.
110         int primaryErrorInt = arguments.getInt("primary_error_int");
111         String urlWithErrors = arguments.getString("url_with_error");
112         String issuedToCName = arguments.getString("issued_to_cname");
113         String issuedToOName = arguments.getString("issued_to_oname");
114         String issuedToUName = arguments.getString("issued_to_uname");
115         String issuedByCName = arguments.getString("issued_by_cname");
116         String issuedByOName = arguments.getString("issued_by_oname");
117         String issuedByUName = arguments.getString("issued_by_uname");
118         String startDate = arguments.getString("start_date");
119         String endDate = arguments.getString("end_date");
120         long webViewFragmentId = arguments.getLong("webview_fragment_id");
121
122         // Get the current position of this WebView fragment.
123         int webViewPosition = MainWebViewActivity.webViewPagerAdapter.getPositionForId(webViewFragmentId);
124
125         // Get the WebView tab fragment.
126         WebViewTabFragment webViewTabFragment = MainWebViewActivity.webViewPagerAdapter.getPageFragment(webViewPosition);
127
128         // Get the fragment view.
129         View fragmentView = webViewTabFragment.getView();
130
131         // Remove the incorrect lint warning below that the fragment view might be null.
132         assert fragmentView != null;
133
134         // Get a handle for the current WebView.
135         NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
136
137         // Get a handle for the SSL error handler.
138         SslErrorHandler sslErrorHandler = nestedScrollWebView.getSslErrorHandler();
139
140         // Remove the incorrect lint warning that `getActivity()` might be null.
141         assert getActivity() != null;
142
143         // Get the activity's layout inflater.
144         LayoutInflater layoutInflater = getActivity().getLayoutInflater();
145
146         // Use an alert dialog builder to create the alert dialog.
147         AlertDialog.Builder dialogBuilder;
148
149         // Get a handle for the shared preferences.
150         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext());
151
152         // Get the screenshot and theme preferences.
153         boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
154         boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false);
155
156         // Set the style and icon according to the theme.
157         if (darkTheme) {
158             // Set the style.
159             dialogBuilder = new AlertDialog.Builder(getActivity(), R.style.PrivacyBrowserAlertDialogDark);
160
161             // Set the icon.
162             dialogBuilder.setIcon(R.drawable.ssl_certificate_enabled_dark);
163         } else {
164             // Set the style.
165             dialogBuilder = new AlertDialog.Builder(getActivity(), R.style.PrivacyBrowserAlertDialogLight);
166
167             // Set the icon.
168             dialogBuilder.setIcon(R.drawable.ssl_certificate_enabled_light);
169         }
170
171         // Set the title.
172         dialogBuilder.setTitle(R.string.ssl_certificate_error);
173
174         // Set the view.  The parent view is `null` because it will be assigned by `AlertDialog`.
175         dialogBuilder.setView(layoutInflater.inflate(R.layout.ssl_certificate_error, null));
176
177         // Set a listener on the cancel button.
178         dialogBuilder.setNegativeButton(R.string.cancel, (DialogInterface dialog, int which) -> {
179             // Check to make sure the SSL error handler is not null.  This might happen if multiple dialogs are displayed at once.
180             if (sslErrorHandler != null) {
181                 // Cancel the request.
182                 sslErrorHandler.cancel();
183
184                 // Reset the SSL error handler.
185                 nestedScrollWebView.resetSslErrorHandler();
186             }
187         });
188
189         // Set a listener on the proceed button.
190         dialogBuilder.setPositiveButton(R.string.proceed, (DialogInterface dialog, int which) -> {
191             // Check to make sure the SSL error handler is not null.  This might happen if multiple dialogs are displayed at once.
192             if (sslErrorHandler != null) {
193                 // Cancel the request.
194                 sslErrorHandler.proceed();
195
196                 // Reset the SSL error handler.
197                 nestedScrollWebView.resetSslErrorHandler();
198             }
199         });
200
201
202         // Create an alert dialog from the alert dialog builder.
203         AlertDialog alertDialog = dialogBuilder.create();
204
205         // Disable screenshots if not allowed.
206         if (!allowScreenshots) {
207             // Remove the warning below that `getWindow()` might be null.
208             assert alertDialog.getWindow() != null;
209
210             // Disable screenshots.
211             alertDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
212         }
213
214         // Get a URI for the URL with errors.
215         Uri uriWithErrors = Uri.parse(urlWithErrors);
216
217         // Get the IP addresses for the URI.
218         new GetIpAddresses(getActivity(), alertDialog).execute(uriWithErrors.getHost());
219
220         // The alert dialog must be shown before the contents can be modified.
221         alertDialog.show();
222
223         // Get handles for the `TextViews`
224         TextView primaryErrorTextView = alertDialog.findViewById(R.id.primary_error);
225         TextView urlTextView = alertDialog.findViewById(R.id.url);
226         TextView issuedToCNameTextView = alertDialog.findViewById(R.id.issued_to_cname);
227         TextView issuedToONameTextView = alertDialog.findViewById(R.id.issued_to_oname);
228         TextView issuedToUNameTextView = alertDialog.findViewById(R.id.issued_to_uname);
229         TextView issuedByTextView = alertDialog.findViewById(R.id.issued_by_textview);
230         TextView issuedByCNameTextView = alertDialog.findViewById(R.id.issued_by_cname);
231         TextView issuedByONameTextView = alertDialog.findViewById(R.id.issued_by_oname);
232         TextView issuedByUNameTextView = alertDialog.findViewById(R.id.issued_by_uname);
233         TextView validDatesTextView = alertDialog.findViewById(R.id.valid_dates_textview);
234         TextView startDateTextView = alertDialog.findViewById(R.id.start_date);
235         TextView endDateTextView = alertDialog.findViewById(R.id.end_date);
236
237         // Setup the common strings.
238         String urlLabel = getString(R.string.url_label) + "  ";
239         String cNameLabel = getString(R.string.common_name) + "  ";
240         String oNameLabel = getString(R.string.organization) + "  ";
241         String uNameLabel = getString(R.string.organizational_unit) + "  ";
242         String startDateLabel = getString(R.string.start_date) + "  ";
243         String endDateLabel = getString(R.string.end_date) + "  ";
244
245         // Create a spannable string builder for each text view that needs multiple colors of text.
246         SpannableStringBuilder urlStringBuilder = new SpannableStringBuilder(urlLabel + urlWithErrors);
247         SpannableStringBuilder issuedToCNameStringBuilder = new SpannableStringBuilder(cNameLabel + issuedToCName);
248         SpannableStringBuilder issuedToONameStringBuilder = new SpannableStringBuilder(oNameLabel + issuedToOName);
249         SpannableStringBuilder issuedToUNameStringBuilder = new SpannableStringBuilder(uNameLabel + issuedToUName);
250         SpannableStringBuilder issuedByCNameStringBuilder = new SpannableStringBuilder(cNameLabel + issuedByCName);
251         SpannableStringBuilder issuedByONameStringBuilder = new SpannableStringBuilder(oNameLabel + issuedByOName);
252         SpannableStringBuilder issuedByUNameStringBuilder = new SpannableStringBuilder(uNameLabel + issuedByUName);
253         SpannableStringBuilder startDateStringBuilder = new SpannableStringBuilder(startDateLabel + startDate);
254         SpannableStringBuilder endDateStringBuilder = new SpannableStringBuilder((endDateLabel + endDate));
255
256         // Create a red foreground color span.  The deprecated `getResources().getColor` must be used until the minimum API >= 23.
257         ForegroundColorSpan redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_a700));
258
259         // Create a blue `ForegroundColorSpan`.
260         ForegroundColorSpan blueColorSpan;
261
262         // Set a blue color span according to the theme.  The deprecated `getResources().getColor` must be used until the minimum API >= 23.
263         if (darkTheme) {
264             blueColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.blue_400));
265         } else {
266             blueColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.blue_700));
267         }
268
269         // Setup the spans to display the certificate information in blue.  `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
270         urlStringBuilder.setSpan(blueColorSpan, urlLabel.length(), urlStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
271         issuedToCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length(), issuedToCNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
272         issuedToONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length(), issuedToONameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
273         issuedToUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length(), issuedToUNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
274         issuedByCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length(), issuedByCNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
275         issuedByONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length(), issuedByONameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
276         issuedByUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length(), issuedByUNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
277         startDateStringBuilder.setSpan(blueColorSpan, startDateLabel.length(), startDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
278         endDateStringBuilder.setSpan(blueColorSpan, endDateLabel.length(), endDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
279
280         // Initialize `primaryErrorString`.
281         String primaryErrorString = "";
282
283         // Highlight the primary error in red and store the primary error string in `primaryErrorString`.
284         switch (primaryErrorInt) {
285             case SslError.SSL_IDMISMATCH:
286                 // Change the URL span colors to red.
287                 urlStringBuilder.setSpan(redColorSpan, urlLabel.length(), urlStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
288                 issuedToCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length(), issuedToCNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
289
290                 // Store the primary error string.
291                 primaryErrorString = getString(R.string.cn_mismatch);
292                 break;
293
294             case SslError.SSL_UNTRUSTED:
295                 // Change the issued by text view text to red.  The deprecated `getResources().getColor` must be used until the minimum API >= 23.
296                 issuedByTextView.setTextColor(getResources().getColor(R.color.red_a700));
297
298                 // Change the issued by span color to red.
299                 issuedByCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length(), issuedByCNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
300                 issuedByONameStringBuilder.setSpan(redColorSpan, oNameLabel.length(), issuedByONameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
301                 issuedByUNameStringBuilder.setSpan(redColorSpan, uNameLabel.length(), issuedByUNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
302
303                 // Store the primary error string.
304                 primaryErrorString = getString(R.string.untrusted);
305                 break;
306
307             case SslError.SSL_DATE_INVALID:
308                 // Change the valid dates text view text to red.  The deprecated `getResources().getColor` must be used until the minimum API >= 23.
309                 validDatesTextView.setTextColor(getResources().getColor(R.color.red_a700));
310
311                 // Change the date span colors to red.
312                 startDateStringBuilder.setSpan(redColorSpan, startDateLabel.length(), startDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
313                 endDateStringBuilder.setSpan(redColorSpan, endDateLabel.length(), endDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
314
315                 // Store the primary error string.
316                 primaryErrorString = getString(R.string.invalid_date);
317                 break;
318
319             case SslError.SSL_NOTYETVALID:
320                 // Change the start date span color to red.
321                 startDateStringBuilder.setSpan(redColorSpan, startDateLabel.length(), startDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
322
323                 // Store the primary error string.
324                 primaryErrorString = getString(R.string.future_certificate);
325                 break;
326
327             case SslError.SSL_EXPIRED:
328                 // Change the end date span color to red.
329                 endDateStringBuilder.setSpan(redColorSpan, endDateLabel.length(), endDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
330
331                 // Store the primary error string.
332                 primaryErrorString = getString(R.string.expired_certificate);
333                 break;
334
335             case SslError.SSL_INVALID:
336                 // Store the primary error string.
337                 primaryErrorString = getString(R.string.invalid_certificate);
338                 break;
339         }
340
341
342         // Display the strings.
343         primaryErrorTextView.setText(primaryErrorString);
344         urlTextView.setText(urlStringBuilder);
345         issuedToCNameTextView.setText(issuedToCNameStringBuilder);
346         issuedToONameTextView.setText(issuedToONameStringBuilder);
347         issuedToUNameTextView.setText(issuedToUNameStringBuilder);
348         issuedByCNameTextView.setText(issuedByCNameStringBuilder);
349         issuedByONameTextView.setText(issuedByONameStringBuilder);
350         issuedByUNameTextView.setText(issuedByUNameStringBuilder);
351         startDateTextView.setText(startDateStringBuilder);
352         endDateTextView.setText(endDateStringBuilder);
353
354         // `onCreateDialog` requires the return of an alert dialog.
355         return alertDialog;
356     }
357
358
359     // This must run asynchronously because it involves a network request.  `String` declares the parameters.  `Void` does not declare progress units.  `SpannableStringBuilder` contains the results.
360     private static class GetIpAddresses extends AsyncTask<String, Void, SpannableStringBuilder> {
361         // The weak references are used to determine if the activity or the alert dialog have disappeared while the AsyncTask is running.
362         private WeakReference<Activity> activityWeakReference;
363         private WeakReference<AlertDialog> alertDialogWeakReference;
364
365         GetIpAddresses(Activity activity, AlertDialog alertDialog) {
366             // Populate the weak references.
367             activityWeakReference = new WeakReference<>(activity);
368             alertDialogWeakReference = new WeakReference<>(alertDialog);
369         }
370
371         @Override
372         protected SpannableStringBuilder doInBackground(String... domainName) {
373             // Get handles for the activity and the alert dialog.
374             Activity activity = activityWeakReference.get();
375             AlertDialog alertDialog = alertDialogWeakReference.get();
376
377             // Abort if the activity or the dialog is gone.
378             if ((activity == null) || (activity.isFinishing()) || (alertDialog == null)) {
379                 return new SpannableStringBuilder();
380             }
381
382             // Initialize an IP address string builder.
383             StringBuilder ipAddresses = new StringBuilder();
384
385             // Get an array with the IP addresses for the host.
386             try {
387                 // Get an array with all the IP addresses for the domain.
388                 InetAddress[] inetAddressesArray = InetAddress.getAllByName(domainName[0]);
389
390                 // Add each IP address to the string builder.
391                 for (InetAddress inetAddress : inetAddressesArray) {
392                     if (ipAddresses.length() == 0) {  // This is the first IP address.
393                         // Add the IP Address to the string builder.
394                         ipAddresses.append(inetAddress.getHostAddress());
395                     } else {  // This is not the first IP address.
396                         // Add a line break to the string builder first.
397                         ipAddresses.append("\n");
398
399                         // Add the IP address to the string builder.
400                         ipAddresses.append(inetAddress.getHostAddress());
401                     }
402                 }
403             } catch (UnknownHostException exception) {
404                 // Do nothing.
405             }
406
407             // Set the label.
408             String ipAddressesLabel = activity.getString(R.string.ip_addresses) + "  ";
409
410             // Create a spannable string builder.
411             SpannableStringBuilder ipAddressesStringBuilder = new SpannableStringBuilder(ipAddressesLabel + ipAddresses);
412
413             // Get a handle for the shared preferences.
414             SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity.getApplicationContext());
415
416             // Get the screenshot and theme preferences.
417             boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
418
419             // Create a blue foreground color span.
420             ForegroundColorSpan blueColorSpan;
421
422             // Set the blue color span according to the theme.  The deprecated `getColor()` must be used until the minimum API >= 23.
423             if (darkTheme) {
424                 blueColorSpan = new ForegroundColorSpan(activity.getResources().getColor(R.color.blue_400));
425             } else {
426                 blueColorSpan = new ForegroundColorSpan(activity.getResources().getColor(R.color.blue_700));
427             }
428
429             // Set the string builder to display the certificate information in blue.  `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
430             ipAddressesStringBuilder.setSpan(blueColorSpan, ipAddressesLabel.length(), ipAddressesStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
431
432             // Return the formatted string.
433             return ipAddressesStringBuilder;
434         }
435
436         // `onPostExecute()` operates on the UI thread.
437         @Override
438         protected void onPostExecute(SpannableStringBuilder ipAddresses) {
439             // Get handles for the activity and the alert dialog.
440             Activity activity = activityWeakReference.get();
441             AlertDialog alertDialog = alertDialogWeakReference.get();
442
443             // Abort if the activity or the alert dialog is gone.
444             if ((activity == null) || (activity.isFinishing()) || (alertDialog == null)) {
445                 return;
446             }
447
448             // Get a handle for the IP addresses text view.
449             TextView ipAddressesTextView = alertDialog.findViewById(R.id.ip_addresses);
450
451             // Populate the IP addresses text view.
452             ipAddressesTextView.setText(ipAddresses);
453         }
454     }
455 }