]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/dialogs/SslCertificateErrorDialog.kt
e44e8026bf29e325985d5f55047f8f63eda8ccfa
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / dialogs / SslCertificateErrorDialog.kt
1 /*
2  * Copyright © 2016-2021 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.Dialog
25 import android.content.DialogInterface
26 import android.content.res.Configuration
27 import android.net.Uri
28 import android.net.http.SslError
29 import android.os.AsyncTask
30 import android.os.Bundle
31 import android.text.SpannableStringBuilder
32 import android.text.Spanned
33 import android.text.style.ForegroundColorSpan
34 import android.view.WindowManager
35 import android.widget.TextView
36
37 import androidx.appcompat.app.AlertDialog
38 import androidx.fragment.app.DialogFragment
39 import androidx.preference.PreferenceManager
40
41 import com.stoutner.privacybrowser.R
42 import com.stoutner.privacybrowser.activities.MainWebViewActivity
43 import com.stoutner.privacybrowser.views.NestedScrollWebView
44
45 import java.lang.ref.WeakReference
46 import java.net.InetAddress
47 import java.net.UnknownHostException
48 import java.text.DateFormat
49
50 // Define the class constants.
51 private const val PRIMARY_ERROR_INT = "primary_error_int"
52 private const val URL_WITH_ERRORS = "url_with_errors"
53 private const val ISSUED_TO_CNAME = "issued_to_cname"
54 private const val ISSUED_TO_ONAME = "issued_to_oname"
55 private const val ISSUED_TO_UNAME = "issued_to_uname"
56 private const val ISSUED_BY_CNAME = "issued_by_cname"
57 private const val ISSUED_BY_ONAME = "issued_by_oname"
58 private const val ISSUED_BY_UNAME = "issued_by_uname"
59 private const val START_DATE = "start_date"
60 private const val END_DATE = "end_date"
61 private const val WEBVIEW_FRAGMENT_ID = "webview_fragment_id"
62
63 class SslCertificateErrorDialog : DialogFragment() {
64     companion object {
65         // `@JvmStatic` will no longer be required once all the code has transitioned to Kotlin.
66         @JvmStatic
67         fun displayDialog(sslError: SslError, webViewFragmentId: Long): SslCertificateErrorDialog {
68             // Get the various components of the SSL error message.
69             val primaryErrorInt = sslError.primaryError
70             val urlWithErrors = sslError.url
71             val sslCertificate = sslError.certificate
72             val issuedToCName = sslCertificate.issuedTo.cName
73             val issuedToOName = sslCertificate.issuedTo.oName
74             val issuedToUName = sslCertificate.issuedTo.uName
75             val issuedByCName = sslCertificate.issuedBy.cName
76             val issuedByOName = sslCertificate.issuedBy.oName
77             val issuedByUName = sslCertificate.issuedBy.uName
78             val startDate = sslCertificate.validNotBeforeDate
79             val endDate = sslCertificate.validNotAfterDate
80
81             // Create an arguments bundle.
82             val argumentsBundle = Bundle()
83
84             // Store the SSL error message components in the bundle.
85             argumentsBundle.putInt(PRIMARY_ERROR_INT, primaryErrorInt)
86             argumentsBundle.putString(URL_WITH_ERRORS, urlWithErrors)
87             argumentsBundle.putString(ISSUED_TO_CNAME, issuedToCName)
88             argumentsBundle.putString(ISSUED_TO_ONAME, issuedToOName)
89             argumentsBundle.putString(ISSUED_TO_UNAME, issuedToUName)
90             argumentsBundle.putString(ISSUED_BY_CNAME, issuedByCName)
91             argumentsBundle.putString(ISSUED_BY_ONAME, issuedByOName)
92             argumentsBundle.putString(ISSUED_BY_UNAME, issuedByUName)
93             argumentsBundle.putString(START_DATE, DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(startDate))
94             argumentsBundle.putString(END_DATE, DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(endDate))
95             argumentsBundle.putLong(WEBVIEW_FRAGMENT_ID, webViewFragmentId)
96
97             // Create a new instance of the SSL certificate error dialog.
98             val thisSslCertificateErrorDialog = SslCertificateErrorDialog()
99
100             // Add the arguments bundle to the new dialog.
101             thisSslCertificateErrorDialog.arguments = argumentsBundle
102
103             // Return the new dialog.
104             return thisSslCertificateErrorDialog
105         }
106     }
107
108     // `@SuppressLint("InflateParams")` removes the warning about using `null` as the parent view group when inflating the alert dialog.
109     @SuppressLint("InflateParams")
110     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
111         // Get the variables from the bundle.
112         val primaryErrorInt = requireArguments().getInt(PRIMARY_ERROR_INT)
113         val urlWithErrors = requireArguments().getString(URL_WITH_ERRORS)
114         val issuedToCName = requireArguments().getString(ISSUED_TO_CNAME)
115         val issuedToOName = requireArguments().getString(ISSUED_TO_ONAME)
116         val issuedToUName = requireArguments().getString(ISSUED_TO_UNAME)
117         val issuedByCName = requireArguments().getString(ISSUED_BY_CNAME)
118         val issuedByOName = requireArguments().getString(ISSUED_BY_ONAME)
119         val issuedByUName = requireArguments().getString(ISSUED_BY_UNAME)
120         val startDate = requireArguments().getString(START_DATE)
121         val endDate = requireArguments().getString(END_DATE)
122         val webViewFragmentId = requireArguments().getLong(WEBVIEW_FRAGMENT_ID)
123
124         // Get the current position of this WebView fragment.
125         val webViewPosition = MainWebViewActivity.webViewPagerAdapter.getPositionForId(webViewFragmentId)
126
127         // Get the WebView tab fragment.
128         val webViewTabFragment = MainWebViewActivity.webViewPagerAdapter.getPageFragment(webViewPosition)
129
130         // Get the fragment view.
131         val fragmentView = webViewTabFragment.requireView()
132
133         // Get a handle for the current WebView.
134         val nestedScrollWebView: NestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview)
135
136         // Get a handle for the SSL error handler.
137         val sslErrorHandler = nestedScrollWebView.sslErrorHandler
138
139         // Use an alert dialog builder to create the alert dialog.
140         val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
141
142         // Get the current theme status.
143         val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
144
145         // Set the icon according to the theme.
146         dialogBuilder.setIconAttribute(R.attr.sslCertificateBlueIcon)
147
148         // Set the title.
149         dialogBuilder.setTitle(R.string.ssl_certificate_error)
150
151         // Set the view.  The parent view is null because it will be assigned by the alert dialog.
152         dialogBuilder.setView(layoutInflater.inflate(R.layout.ssl_certificate_error, null))
153
154         // Set the cancel button listener.
155         dialogBuilder.setNegativeButton(R.string.cancel) { _: DialogInterface?, _: Int ->
156             // Check to make sure the SSL error handler is not null.  This might happen if multiple dialogs are displayed at once.
157             if (sslErrorHandler != null) {
158                 // Cancel the request.
159                 sslErrorHandler.cancel()
160
161                 // Reset the SSL error handler.
162                 nestedScrollWebView.resetSslErrorHandler()
163             }
164         }
165
166         // Set the proceed button listener.
167         dialogBuilder.setPositiveButton(R.string.proceed) { _: DialogInterface?, _: Int ->
168             // Check to make sure the SSL error handler is not null.  This might happen if multiple dialogs are displayed at once.
169             if (sslErrorHandler != null) {
170                 // Proceed to the website.
171                 sslErrorHandler.proceed()
172
173                 // Reset the SSL error handler.
174                 nestedScrollWebView.resetSslErrorHandler()
175             }
176         }
177
178         // Create an alert dialog from the builder.
179         val alertDialog = dialogBuilder.create()
180
181         // Get a handle for the shared preferences.
182         val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
183
184         // Get the screenshot preference.
185         val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
186
187         // Disable screenshots if not allowed.
188         if (!allowScreenshots) {
189             // Disable screenshots.
190             alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
191         }
192
193         // Get a URI for the URL with errors.
194         val uriWithErrors = Uri.parse(urlWithErrors)
195
196         // Get the IP addresses for the URI.
197         GetIpAddresses(requireActivity(), alertDialog).execute(uriWithErrors.host)
198
199         // The alert dialog must be shown before the contents can be modified.
200         alertDialog.show()
201
202         // Get handles for the views.
203         val primaryErrorTextView = alertDialog.findViewById<TextView>(R.id.primary_error)!!
204         val urlTextView = alertDialog.findViewById<TextView>(R.id.url)!!
205         val issuedToCNameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_cname)!!
206         val issuedToONameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_oname)!!
207         val issuedToUNameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_uname)!!
208         val issuedByTextView = alertDialog.findViewById<TextView>(R.id.issued_by_textview)!!
209         val issuedByCNameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_cname)!!
210         val issuedByONameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_oname)!!
211         val issuedByUNameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_uname)!!
212         val validDatesTextView = alertDialog.findViewById<TextView>(R.id.valid_dates_textview)!!
213         val startDateTextView = alertDialog.findViewById<TextView>(R.id.start_date)!!
214         val endDateTextView = alertDialog.findViewById<TextView>(R.id.end_date)!!
215
216         // Setup the common strings.
217         val urlLabel = getString(R.string.url_label) + "  "
218         val cNameLabel = getString(R.string.common_name) + "  "
219         val oNameLabel = getString(R.string.organization) + "  "
220         val uNameLabel = getString(R.string.organizational_unit) + "  "
221         val startDateLabel = getString(R.string.start_date) + "  "
222         val endDateLabel = getString(R.string.end_date) + "  "
223
224         // Create a spannable string builder for each text view that needs multiple colors of text.
225         val urlStringBuilder = SpannableStringBuilder(urlLabel + urlWithErrors)
226         val issuedToCNameStringBuilder = SpannableStringBuilder(cNameLabel + issuedToCName)
227         val issuedToONameStringBuilder = SpannableStringBuilder(oNameLabel + issuedToOName)
228         val issuedToUNameStringBuilder = SpannableStringBuilder(uNameLabel + issuedToUName)
229         val issuedByCNameStringBuilder = SpannableStringBuilder(cNameLabel + issuedByCName)
230         val issuedByONameStringBuilder = SpannableStringBuilder(oNameLabel + issuedByOName)
231         val issuedByUNameStringBuilder = SpannableStringBuilder(uNameLabel + issuedByUName)
232         val startDateStringBuilder = SpannableStringBuilder(startDateLabel + startDate)
233         val endDateStringBuilder = SpannableStringBuilder(endDateLabel + endDate)
234
235         // Define the color spans.
236         val blueColorSpan: ForegroundColorSpan
237         val redColorSpan: ForegroundColorSpan
238
239         // Set the color spans according to the theme.  The deprecated `getColor()` must be used until the minimum API >= 23.
240         if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
241             @Suppress("DEPRECATION")
242             blueColorSpan = ForegroundColorSpan(resources.getColor(R.color.blue_700))
243             @Suppress("DEPRECATION")
244             redColorSpan = ForegroundColorSpan(resources.getColor(R.color.red_a700))
245         } else {
246             @Suppress("DEPRECATION")
247             blueColorSpan = ForegroundColorSpan(resources.getColor(R.color.violet_700))
248             @Suppress("DEPRECATION")
249             redColorSpan = ForegroundColorSpan(resources.getColor(R.color.red_900))
250         }
251
252         // Setup the spans to display the certificate information in blue.  `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
253         urlStringBuilder.setSpan(blueColorSpan, urlLabel.length, urlStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
254         issuedToCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
255         issuedToONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length, issuedToONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
256         issuedToUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length, issuedToUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
257         issuedByCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedByCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
258         issuedByONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length, issuedByONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
259         issuedByUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length, issuedByUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
260         startDateStringBuilder.setSpan(blueColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
261         endDateStringBuilder.setSpan(blueColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
262
263         // Define the primary error string.
264         var primaryErrorString = ""
265
266         // Highlight the primary error in red and store it in the primary error string.
267         when (primaryErrorInt) {
268             SslError.SSL_IDMISMATCH -> {
269                 // Change the URL span colors to red.
270                 urlStringBuilder.setSpan(redColorSpan, urlLabel.length, urlStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
271                 issuedToCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
272
273                 // Store the primary error string.
274                 primaryErrorString = getString(R.string.cn_mismatch)
275             }
276
277             SslError.SSL_UNTRUSTED -> {
278                 // Change the issued by text view text to red.  The deprecated `getColor()` must be used until the minimum API >= 23.
279                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
280                     @Suppress("DEPRECATION")
281                     issuedByTextView.setTextColor(resources.getColor(R.color.red_a700))
282                 } else {
283                     @Suppress("DEPRECATION")
284                     issuedByTextView.setTextColor(resources.getColor(R.color.red_900))
285                 }
286
287                 // Change the issued by span color to red.
288                 issuedByCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length, issuedByCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
289                 issuedByONameStringBuilder.setSpan(redColorSpan, oNameLabel.length, issuedByONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
290                 issuedByUNameStringBuilder.setSpan(redColorSpan, uNameLabel.length, issuedByUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
291
292                 // Store the primary error string.
293                 primaryErrorString = getString(R.string.untrusted)
294             }
295
296             SslError.SSL_DATE_INVALID -> {
297                 // Change the valid dates text view text to red.  The deprecated `getColor()` must be used until the minimum API >= 23.
298                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
299                     @Suppress("DEPRECATION")
300                     validDatesTextView.setTextColor(resources.getColor(R.color.red_a700))
301                 } else {
302                     @Suppress("DEPRECATION")
303                     validDatesTextView.setTextColor(resources.getColor(R.color.red_900))
304                 }
305
306                 // Change the date span colors to red.
307                 startDateStringBuilder.setSpan(redColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
308                 endDateStringBuilder.setSpan(redColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
309
310                 // Store the primary error string.
311                 primaryErrorString = getString(R.string.invalid_date)
312             }
313
314             SslError.SSL_NOTYETVALID -> {
315                 // Change the start date span color to red.
316                 startDateStringBuilder.setSpan(redColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
317
318                 // Store the primary error string.
319                 primaryErrorString = getString(R.string.future_certificate)
320             }
321
322             SslError.SSL_EXPIRED -> {
323                 // Change the end date span color to red.
324                 endDateStringBuilder.setSpan(redColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
325
326                 // Store the primary error string.
327                 primaryErrorString = getString(R.string.expired_certificate)
328             }
329
330             SslError.SSL_INVALID ->
331                 // Store the primary error string.
332                 primaryErrorString = getString(R.string.invalid_certificate)
333         }
334
335         // Display the strings.
336         primaryErrorTextView.text = primaryErrorString
337         urlTextView.text = urlStringBuilder
338         issuedToCNameTextView.text = issuedToCNameStringBuilder
339         issuedToONameTextView.text = issuedToONameStringBuilder
340         issuedToUNameTextView.text = issuedToUNameStringBuilder
341         issuedByCNameTextView.text = issuedByCNameStringBuilder
342         issuedByONameTextView.text = issuedByONameStringBuilder
343         issuedByUNameTextView.text = issuedByUNameStringBuilder
344         startDateTextView.text = startDateStringBuilder
345         endDateTextView.text = endDateStringBuilder
346
347         // Return the alert dialog.
348         return alertDialog
349     }
350
351     // This must run asynchronously because it involves a network request.  `String` declares the parameters.  `Void` does not declare progress units.  `SpannableStringBuilder` contains the results.
352     private class GetIpAddresses constructor(activity: Activity, alertDialog: AlertDialog) : AsyncTask<String, Void?, SpannableStringBuilder>() {
353         // Define the weak references.
354         private val activityWeakReference: WeakReference<Activity> = WeakReference(activity)
355         private val alertDialogWeakReference: WeakReference<AlertDialog> = WeakReference(alertDialog)
356
357         override fun doInBackground(vararg domainName: String): SpannableStringBuilder {
358             // Get handles for the activity and the alert dialog.
359             val activity = activityWeakReference.get()
360             val alertDialog = alertDialogWeakReference.get()
361
362             // Abort if the activity or the dialog is gone.
363             if (activity == null || activity.isFinishing || alertDialog == null) {
364                 return SpannableStringBuilder()
365             }
366
367             // Initialize an IP address string builder.
368             val ipAddresses = StringBuilder()
369
370             // Get an array with the IP addresses for the host.
371             try {
372                 // Get an array with all the IP addresses for the domain.
373                 val inetAddressesArray = InetAddress.getAllByName(domainName[0])
374
375                 // Add each IP address to the string builder.
376                 for (inetAddress in inetAddressesArray) {
377                     // Check to see if this is not the first IP address.
378                     if (ipAddresses.isNotEmpty()) {
379                         // Add a line break to the string builder first.
380                         ipAddresses.append("\n")
381                     }
382
383                     // Add the IP Address to the string builder.
384                     ipAddresses.append(inetAddress.hostAddress)
385                 }
386             } catch (exception: UnknownHostException) {
387                 // Do nothing.
388             }
389
390             // Set the label.
391             val ipAddressesLabel = activity.getString(R.string.ip_addresses) + "  "
392
393             // Create a spannable string builder.
394             val ipAddressesStringBuilder = SpannableStringBuilder(ipAddressesLabel + ipAddresses)
395
396             // Create a blue foreground color span.
397             val blueColorSpan: ForegroundColorSpan
398
399             // Get the current theme status.
400             val currentThemeStatus = activity.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
401
402             // Set the blue color span according to the theme.  The deprecated `getColor()` must be used until the minimum API >= 23.
403             blueColorSpan = if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
404                 @Suppress("DEPRECATION")
405                 ForegroundColorSpan(activity.resources.getColor(R.color.blue_700))
406             } else {
407                 @Suppress("DEPRECATION")
408                 ForegroundColorSpan(activity.resources.getColor(R.color.violet_500))
409             }
410
411             // Set the string builder to display the certificate information in blue.  `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
412             ipAddressesStringBuilder.setSpan(blueColorSpan, ipAddressesLabel.length, ipAddressesStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
413
414             // Return the formatted string.
415             return ipAddressesStringBuilder
416         }
417
418         // `onPostExecute()` operates on the UI thread.
419         override fun onPostExecute(ipAddresses: SpannableStringBuilder) {
420             // Get handles for the activity and the alert dialog.
421             val activity = activityWeakReference.get()
422             val alertDialog = alertDialogWeakReference.get()
423
424             // Abort if the activity or the alert dialog is gone.
425             if (activity == null || activity.isFinishing || alertDialog == null) {
426                 return
427             }
428
429             // Get a handle for the IP addresses text view.
430             val ipAddressesTextView = alertDialog.findViewById<TextView>(R.id.ip_addresses)!!
431
432             // Populate the IP addresses text view.
433             ipAddressesTextView.text = ipAddresses
434         }
435     }
436 }