2 * Copyright © 2016-2020 Soren Stoutner <soren@stoutner.com>.
4 * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
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.
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.
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/>.
20 package com.stoutner.privacybrowser.dialogs;
22 import android.annotation.SuppressLint;
23 import android.app.Activity;
24 import android.app.Dialog;
25 import android.content.DialogInterface;
26 import android.content.SharedPreferences;
27 import android.content.res.Configuration;
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;
43 import androidx.annotation.NonNull;
44 import androidx.appcompat.app.AlertDialog;
45 import androidx.fragment.app.DialogFragment;
47 import com.stoutner.privacybrowser.R;
48 import com.stoutner.privacybrowser.activities.MainWebViewActivity;
49 import com.stoutner.privacybrowser.fragments.WebViewTabFragment;
50 import com.stoutner.privacybrowser.views.NestedScrollWebView;
52 import java.lang.ref.WeakReference;
53 import java.net.InetAddress;
54 import java.net.UnknownHostException;
55 import java.text.DateFormat;
56 import java.util.Date;
58 public class SslCertificateErrorDialog extends DialogFragment {
59 public static SslCertificateErrorDialog displayDialog(SslError error, long webViewFragmentId) {
60 // Get the various components of the SSL error message.
61 int primaryErrorIntForBundle = error.getPrimaryError();
62 String urlWithErrorForBundle = error.getUrl();
63 SslCertificate sslCertificate = error.getCertificate();
64 String issuedToCNameForBundle = sslCertificate.getIssuedTo().getCName();
65 String issuedToONameForBundle = sslCertificate.getIssuedTo().getOName();
66 String issuedToUNameForBundle = sslCertificate.getIssuedTo().getUName();
67 String issuedByCNameForBundle = sslCertificate.getIssuedBy().getCName();
68 String issuedByONameForBundle = sslCertificate.getIssuedBy().getOName();
69 String issuedByUNameForBundle = sslCertificate.getIssuedBy().getUName();
70 Date startDateForBundle = sslCertificate.getValidNotBeforeDate();
71 Date endDateForBundle = sslCertificate.getValidNotAfterDate();
73 // Create an arguments bundle.
74 Bundle argumentsBundle = new Bundle();
76 // Store the SSL error message components in a `Bundle`.
77 argumentsBundle.putInt("primary_error_int", primaryErrorIntForBundle);
78 argumentsBundle.putString("url_with_error", urlWithErrorForBundle);
79 argumentsBundle.putString("issued_to_cname", issuedToCNameForBundle);
80 argumentsBundle.putString("issued_to_oname", issuedToONameForBundle);
81 argumentsBundle.putString("issued_to_uname", issuedToUNameForBundle);
82 argumentsBundle.putString("issued_by_cname", issuedByCNameForBundle);
83 argumentsBundle.putString("issued_by_oname", issuedByONameForBundle);
84 argumentsBundle.putString("issued_by_uname", issuedByUNameForBundle);
85 argumentsBundle.putString("start_date", DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(startDateForBundle));
86 argumentsBundle.putString("end_date", DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(endDateForBundle));
87 argumentsBundle.putLong("webview_fragment_id", webViewFragmentId);
89 // Create a new instance of the SSL certificate error dialog.
90 SslCertificateErrorDialog thisSslCertificateErrorDialog = new SslCertificateErrorDialog();
92 // Add the arguments bundle to the new dialog.
93 thisSslCertificateErrorDialog.setArguments(argumentsBundle);
95 // Return the new dialog.
96 return thisSslCertificateErrorDialog;
99 // `@SuppressLint("InflateParams")` removes the warning about using `null` as the parent view group when inflating the `AlertDialog`.
100 @SuppressLint("InflateParams")
103 public Dialog onCreateDialog(Bundle savedInstanceState) {
104 // Get a handle for the arguments.
105 Bundle arguments = getArguments();
107 // Remove the incorrect lint warning that the arguments might be null.
108 assert arguments != null;
110 // Get the variables from the bundle.
111 int primaryErrorInt = arguments.getInt("primary_error_int");
112 String urlWithErrors = arguments.getString("url_with_error");
113 String issuedToCName = arguments.getString("issued_to_cname");
114 String issuedToOName = arguments.getString("issued_to_oname");
115 String issuedToUName = arguments.getString("issued_to_uname");
116 String issuedByCName = arguments.getString("issued_by_cname");
117 String issuedByOName = arguments.getString("issued_by_oname");
118 String issuedByUName = arguments.getString("issued_by_uname");
119 String startDate = arguments.getString("start_date");
120 String endDate = arguments.getString("end_date");
121 long webViewFragmentId = arguments.getLong("webview_fragment_id");
123 // Get the current position of this WebView fragment.
124 int webViewPosition = MainWebViewActivity.webViewPagerAdapter.getPositionForId(webViewFragmentId);
126 // Get the WebView tab fragment.
127 WebViewTabFragment webViewTabFragment = MainWebViewActivity.webViewPagerAdapter.getPageFragment(webViewPosition);
129 // Get the fragment view.
130 View fragmentView = webViewTabFragment.getView();
132 // Remove the incorrect lint warning below that the fragment view might be null.
133 assert fragmentView != null;
135 // Get a handle for the current WebView.
136 NestedScrollWebView nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview);
138 // Get a handle for the SSL error handler.
139 SslErrorHandler sslErrorHandler = nestedScrollWebView.getSslErrorHandler();
141 // Get the activity's layout inflater.
142 LayoutInflater layoutInflater = requireActivity().getLayoutInflater();
144 // Use an alert dialog builder to create the alert dialog.
145 AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog);
147 // Get the current theme status.
148 int currentThemeStatus = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
150 // Set the icon according to the theme.
151 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES) {
152 dialogBuilder.setIcon(R.drawable.ssl_certificate_enabled_night);
154 dialogBuilder.setIcon(R.drawable.ssl_certificate_enabled_day);
158 dialogBuilder.setTitle(R.string.ssl_certificate_error);
160 // Set the view. The parent view is `null` because it will be assigned by `AlertDialog`.
161 dialogBuilder.setView(layoutInflater.inflate(R.layout.ssl_certificate_error, null));
163 // Set a listener on the cancel button.
164 dialogBuilder.setNegativeButton(R.string.cancel, (DialogInterface dialog, int which) -> {
165 // Check to make sure the SSL error handler is not null. This might happen if multiple dialogs are displayed at once.
166 if (sslErrorHandler != null) {
167 // Cancel the request.
168 sslErrorHandler.cancel();
170 // Reset the SSL error handler.
171 nestedScrollWebView.resetSslErrorHandler();
175 // Set a listener on the proceed button.
176 dialogBuilder.setPositiveButton(R.string.proceed, (DialogInterface dialog, int which) -> {
177 // Check to make sure the SSL error handler is not null. This might happen if multiple dialogs are displayed at once.
178 if (sslErrorHandler != null) {
179 // Cancel the request.
180 sslErrorHandler.proceed();
182 // Reset the SSL error handler.
183 nestedScrollWebView.resetSslErrorHandler();
188 // Create an alert dialog from the alert dialog builder.
189 AlertDialog alertDialog = dialogBuilder.create();
191 // Get a handle for the shared preferences.
192 SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext());
194 // Get the screenshot preference.
195 boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false);
197 // Disable screenshots if not allowed.
198 if (!allowScreenshots) {
199 // Remove the warning below that `getWindow()` might be null.
200 assert alertDialog.getWindow() != null;
202 // Disable screenshots.
203 alertDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
206 // Get a URI for the URL with errors.
207 Uri uriWithErrors = Uri.parse(urlWithErrors);
209 // Get the IP addresses for the URI.
210 new GetIpAddresses(getActivity(), alertDialog).execute(uriWithErrors.getHost());
212 // The alert dialog must be shown before the contents can be modified.
215 // Get handles for the `TextViews`
216 TextView primaryErrorTextView = alertDialog.findViewById(R.id.primary_error);
217 TextView urlTextView = alertDialog.findViewById(R.id.url);
218 TextView issuedToCNameTextView = alertDialog.findViewById(R.id.issued_to_cname);
219 TextView issuedToONameTextView = alertDialog.findViewById(R.id.issued_to_oname);
220 TextView issuedToUNameTextView = alertDialog.findViewById(R.id.issued_to_uname);
221 TextView issuedByTextView = alertDialog.findViewById(R.id.issued_by_textview);
222 TextView issuedByCNameTextView = alertDialog.findViewById(R.id.issued_by_cname);
223 TextView issuedByONameTextView = alertDialog.findViewById(R.id.issued_by_oname);
224 TextView issuedByUNameTextView = alertDialog.findViewById(R.id.issued_by_uname);
225 TextView validDatesTextView = alertDialog.findViewById(R.id.valid_dates_textview);
226 TextView startDateTextView = alertDialog.findViewById(R.id.start_date);
227 TextView endDateTextView = alertDialog.findViewById(R.id.end_date);
229 // Remove the incorrect lint warnings below that the views might be null.
230 assert primaryErrorTextView != null;
231 assert urlTextView != null;
232 assert issuedToCNameTextView != null;
233 assert issuedToONameTextView != null;
234 assert issuedToUNameTextView != null;
235 assert issuedByTextView != null;
236 assert issuedByCNameTextView != null;
237 assert issuedByONameTextView != null;
238 assert issuedByUNameTextView != null;
239 assert validDatesTextView != null;
240 assert startDateTextView != null;
241 assert endDateTextView != null;
243 // Setup the common strings.
244 String urlLabel = getString(R.string.url_label) + " ";
245 String cNameLabel = getString(R.string.common_name) + " ";
246 String oNameLabel = getString(R.string.organization) + " ";
247 String uNameLabel = getString(R.string.organizational_unit) + " ";
248 String startDateLabel = getString(R.string.start_date) + " ";
249 String endDateLabel = getString(R.string.end_date) + " ";
251 // Create a spannable string builder for each text view that needs multiple colors of text.
252 SpannableStringBuilder urlStringBuilder = new SpannableStringBuilder(urlLabel + urlWithErrors);
253 SpannableStringBuilder issuedToCNameStringBuilder = new SpannableStringBuilder(cNameLabel + issuedToCName);
254 SpannableStringBuilder issuedToONameStringBuilder = new SpannableStringBuilder(oNameLabel + issuedToOName);
255 SpannableStringBuilder issuedToUNameStringBuilder = new SpannableStringBuilder(uNameLabel + issuedToUName);
256 SpannableStringBuilder issuedByCNameStringBuilder = new SpannableStringBuilder(cNameLabel + issuedByCName);
257 SpannableStringBuilder issuedByONameStringBuilder = new SpannableStringBuilder(oNameLabel + issuedByOName);
258 SpannableStringBuilder issuedByUNameStringBuilder = new SpannableStringBuilder(uNameLabel + issuedByUName);
259 SpannableStringBuilder startDateStringBuilder = new SpannableStringBuilder(startDateLabel + startDate);
260 SpannableStringBuilder endDateStringBuilder = new SpannableStringBuilder((endDateLabel + endDate));
262 // Define the color spans.
263 ForegroundColorSpan blueColorSpan;
264 ForegroundColorSpan redColorSpan;
266 // Set the color spans according to the theme. The deprecated `getResources()` must be used until the minimum API >= 23.
267 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
268 blueColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.blue_700));
269 redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_a700));
271 blueColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.violet_500));
272 redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_900));
275 // Setup the spans to display the certificate information in blue. `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
276 urlStringBuilder.setSpan(blueColorSpan, urlLabel.length(), urlStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
277 issuedToCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length(), issuedToCNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
278 issuedToONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length(), issuedToONameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
279 issuedToUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length(), issuedToUNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
280 issuedByCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length(), issuedByCNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
281 issuedByONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length(), issuedByONameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
282 issuedByUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length(), issuedByUNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
283 startDateStringBuilder.setSpan(blueColorSpan, startDateLabel.length(), startDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
284 endDateStringBuilder.setSpan(blueColorSpan, endDateLabel.length(), endDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
286 // Initialize `primaryErrorString`.
287 String primaryErrorString = "";
289 // Highlight the primary error in red and store the primary error string in `primaryErrorString`.
290 switch (primaryErrorInt) {
291 case SslError.SSL_IDMISMATCH:
292 // Change the URL span colors to red.
293 urlStringBuilder.setSpan(redColorSpan, urlLabel.length(), urlStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
294 issuedToCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length(), issuedToCNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
296 // Store the primary error string.
297 primaryErrorString = getString(R.string.cn_mismatch);
300 case SslError.SSL_UNTRUSTED:
301 // Change the issued by text view text to red. The deprecated `getResources().getColor` must be used until the minimum API >= 23.
302 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES) {
303 issuedByTextView.setTextColor(getResources().getColor(R.color.red_900));
305 issuedByTextView.setTextColor(getResources().getColor(R.color.red_a700));
308 // Change the issued by span color to red.
309 issuedByCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length(), issuedByCNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
310 issuedByONameStringBuilder.setSpan(redColorSpan, oNameLabel.length(), issuedByONameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
311 issuedByUNameStringBuilder.setSpan(redColorSpan, uNameLabel.length(), issuedByUNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
313 // Store the primary error string.
314 primaryErrorString = getString(R.string.untrusted);
317 case SslError.SSL_DATE_INVALID:
318 // Change the valid dates text view text to red. The deprecated `getResources().getColor` must be used until the minimum API >= 23.
319 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES) {
320 validDatesTextView.setTextColor(getResources().getColor(R.color.red_900));
322 validDatesTextView.setTextColor(getResources().getColor(R.color.red_a700));
325 // Change the date span colors to red.
326 startDateStringBuilder.setSpan(redColorSpan, startDateLabel.length(), startDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
327 endDateStringBuilder.setSpan(redColorSpan, endDateLabel.length(), endDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
329 // Store the primary error string.
330 primaryErrorString = getString(R.string.invalid_date);
333 case SslError.SSL_NOTYETVALID:
334 // Change the start date span color to red.
335 startDateStringBuilder.setSpan(redColorSpan, startDateLabel.length(), startDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
337 // Store the primary error string.
338 primaryErrorString = getString(R.string.future_certificate);
341 case SslError.SSL_EXPIRED:
342 // Change the end date span color to red.
343 endDateStringBuilder.setSpan(redColorSpan, endDateLabel.length(), endDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
345 // Store the primary error string.
346 primaryErrorString = getString(R.string.expired_certificate);
349 case SslError.SSL_INVALID:
350 // Store the primary error string.
351 primaryErrorString = getString(R.string.invalid_certificate);
356 // Display the strings.
357 primaryErrorTextView.setText(primaryErrorString);
358 urlTextView.setText(urlStringBuilder);
359 issuedToCNameTextView.setText(issuedToCNameStringBuilder);
360 issuedToONameTextView.setText(issuedToONameStringBuilder);
361 issuedToUNameTextView.setText(issuedToUNameStringBuilder);
362 issuedByCNameTextView.setText(issuedByCNameStringBuilder);
363 issuedByONameTextView.setText(issuedByONameStringBuilder);
364 issuedByUNameTextView.setText(issuedByUNameStringBuilder);
365 startDateTextView.setText(startDateStringBuilder);
366 endDateTextView.setText(endDateStringBuilder);
368 // `onCreateDialog` requires the return of an alert dialog.
373 // This must run asynchronously because it involves a network request. `String` declares the parameters. `Void` does not declare progress units. `SpannableStringBuilder` contains the results.
374 private static class GetIpAddresses extends AsyncTask<String, Void, SpannableStringBuilder> {
375 // The weak references are used to determine if the activity or the alert dialog have disappeared while the AsyncTask is running.
376 private WeakReference<Activity> activityWeakReference;
377 private WeakReference<AlertDialog> alertDialogWeakReference;
379 GetIpAddresses(Activity activity, AlertDialog alertDialog) {
380 // Populate the weak references.
381 activityWeakReference = new WeakReference<>(activity);
382 alertDialogWeakReference = new WeakReference<>(alertDialog);
386 protected SpannableStringBuilder doInBackground(String... domainName) {
387 // Get handles for the activity and the alert dialog.
388 Activity activity = activityWeakReference.get();
389 AlertDialog alertDialog = alertDialogWeakReference.get();
391 // Abort if the activity or the dialog is gone.
392 if ((activity == null) || (activity.isFinishing()) || (alertDialog == null)) {
393 return new SpannableStringBuilder();
396 // Initialize an IP address string builder.
397 StringBuilder ipAddresses = new StringBuilder();
399 // Get an array with the IP addresses for the host.
401 // Get an array with all the IP addresses for the domain.
402 InetAddress[] inetAddressesArray = InetAddress.getAllByName(domainName[0]);
404 // Add each IP address to the string builder.
405 for (InetAddress inetAddress : inetAddressesArray) {
406 // Check to see if this is not the first IP address.
407 if (ipAddresses.length() > 0) {
408 // Add a line break to the string builder first.
409 ipAddresses.append("\n");
412 // Add the IP Address to the string builder.
413 ipAddresses.append(inetAddress.getHostAddress());
415 } catch (UnknownHostException exception) {
420 String ipAddressesLabel = activity.getString(R.string.ip_addresses) + " ";
422 // Create a spannable string builder.
423 SpannableStringBuilder ipAddressesStringBuilder = new SpannableStringBuilder(ipAddressesLabel + ipAddresses);
425 // Create a blue foreground color span.
426 ForegroundColorSpan blueColorSpan;
428 // Get the current theme status.
429 int currentThemeStatus = activity.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
431 // Set the blue color span according to the theme. The deprecated `getColor()` must be used until the minimum API >= 23.
432 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_YES) {
433 blueColorSpan = new ForegroundColorSpan(activity.getResources().getColor(R.color.violet_500));
435 blueColorSpan = new ForegroundColorSpan(activity.getResources().getColor(R.color.blue_700));
438 // Set the string builder to display the certificate information in blue. `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
439 ipAddressesStringBuilder.setSpan(blueColorSpan, ipAddressesLabel.length(), ipAddressesStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
441 // Return the formatted string.
442 return ipAddressesStringBuilder;
445 // `onPostExecute()` operates on the UI thread.
447 protected void onPostExecute(SpannableStringBuilder ipAddresses) {
448 // Get handles for the activity and the alert dialog.
449 Activity activity = activityWeakReference.get();
450 AlertDialog alertDialog = alertDialogWeakReference.get();
452 // Abort if the activity or the alert dialog is gone.
453 if ((activity == null) || (activity.isFinishing()) || (alertDialog == null)) {
457 // Get a handle for the IP addresses text view.
458 TextView ipAddressesTextView = alertDialog.findViewById(R.id.ip_addresses);
460 // Remove the incorrect lint warning below that the view might be null.
461 assert ipAddressesTextView != null;
463 // Populate the IP addresses text view.
464 ipAddressesTextView.setText(ipAddresses);