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