2 * Copyright 2016-2022 Soren Stoutner <soren@stoutner.com>.
4 * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
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.
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.
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/>.
20 package com.stoutner.privacybrowser.dialogs
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
35 import androidx.appcompat.app.AlertDialog
36 import androidx.fragment.app.DialogFragment
37 import androidx.preference.PreferenceManager
39 import com.stoutner.privacybrowser.R
40 import com.stoutner.privacybrowser.activities.MainWebViewActivity
41 import com.stoutner.privacybrowser.views.NestedScrollWebView
43 import java.lang.ref.WeakReference
44 import java.net.InetAddress
45 import java.net.UnknownHostException
46 import java.text.DateFormat
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"
61 class SslCertificateErrorDialog : DialogFragment() {
63 // `@JvmStatic` will no longer be required once all the code has transitioned to Kotlin.
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
79 // Create an arguments bundle.
80 val argumentsBundle = Bundle()
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)
95 // Create a new instance of the SSL certificate error dialog.
96 val thisSslCertificateErrorDialog = SslCertificateErrorDialog()
98 // Add the arguments bundle to the new dialog.
99 thisSslCertificateErrorDialog.arguments = argumentsBundle
101 // Return the new dialog.
102 return thisSslCertificateErrorDialog
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)
120 // Get the current position of this WebView fragment.
121 val webViewPosition = MainWebViewActivity.webViewPagerAdapter.getPositionForId(webViewFragmentId)
123 // Get the WebView tab fragment.
124 val webViewTabFragment = MainWebViewActivity.webViewPagerAdapter.getPageFragment(webViewPosition)
126 // Get the fragment view.
127 val fragmentView = webViewTabFragment.requireView()
129 // Get a handle for the current WebView.
130 val nestedScrollWebView: NestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview)
132 // Get a handle for the SSL error handler.
133 val sslErrorHandler = nestedScrollWebView.sslErrorHandler
135 // Use an alert dialog builder to create the alert dialog.
136 val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
139 dialogBuilder.setIcon(R.drawable.ssl_certificate)
142 dialogBuilder.setTitle(R.string.ssl_certificate_error)
145 dialogBuilder.setView(R.layout.ssl_certificate_error)
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()
154 // Reset the SSL error handler.
155 nestedScrollWebView.resetSslErrorHandler()
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()
166 // Reset the SSL error handler.
167 nestedScrollWebView.resetSslErrorHandler()
171 // Create an alert dialog from the builder.
172 val alertDialog = dialogBuilder.create()
174 // Get a handle for the shared preferences.
175 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
177 // Get the screenshot preference.
178 val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
180 // Disable screenshots if not allowed.
181 if (!allowScreenshots) {
182 // Disable screenshots.
183 alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
186 // Get a URI for the URL with errors.
187 val uriWithErrors = Uri.parse(urlWithErrors)
189 // Get the IP addresses for the URI.
190 GetIpAddresses(requireActivity(), alertDialog).execute(uriWithErrors.host)
192 // The alert dialog must be shown before the contents can be modified.
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)!!
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) + " "
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)
228 // Define the color spans.
229 val blueColorSpan = ForegroundColorSpan(requireContext().getColor(R.color.alt_blue_text))
230 val redColorSpan = ForegroundColorSpan(requireContext().getColor(R.color.red_text))
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)
243 // Define the primary error string.
244 var primaryErrorString = ""
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)
253 // Store the primary error string.
254 primaryErrorString = getString(R.string.cn_mismatch)
257 SslError.SSL_UNTRUSTED -> {
258 // Change the issued by text view text to red.
259 issuedByTextView.setTextColor(requireContext().getColor(R.color.red_text))
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)
266 // Store the primary error string.
267 primaryErrorString = getString(R.string.untrusted)
270 SslError.SSL_DATE_INVALID -> {
271 // Change the valid dates text view text to red.
272 validDatesTextView.setTextColor(requireContext().getColor(R.color.red_text))
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)
278 // Store the primary error string.
279 primaryErrorString = getString(R.string.invalid_date)
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)
286 // Store the primary error string.
287 primaryErrorString = getString(R.string.future_certificate)
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)
294 // Store the primary error string.
295 primaryErrorString = getString(R.string.expired_certificate)
298 SslError.SSL_INVALID ->
299 // Store the primary error string.
300 primaryErrorString = getString(R.string.invalid_certificate)
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
315 // Return the alert dialog.
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)
325 @Deprecated("Deprecated in Java")
326 override fun doInBackground(vararg domainName: String): SpannableStringBuilder {
327 // Get handles for the activity and the alert dialog.
328 val activity = activityWeakReference.get()
329 val alertDialog = alertDialogWeakReference.get()
331 // Abort if the activity or the dialog is gone.
332 if (activity == null || activity.isFinishing || alertDialog == null) {
333 return SpannableStringBuilder()
336 // Initialize an IP address string builder.
337 val ipAddresses = StringBuilder()
339 // Get an array with the IP addresses for the host.
341 // Get an array with all the IP addresses for the domain.
342 val inetAddressesArray = InetAddress.getAllByName(domainName[0])
344 // Add each IP address to the string builder.
345 for (inetAddress in inetAddressesArray) {
346 // Check to see if this is not the first IP address.
347 if (ipAddresses.isNotEmpty()) {
348 // Add a line break to the string builder first.
349 ipAddresses.append("\n")
352 // Add the IP Address to the string builder.
353 ipAddresses.append(inetAddress.hostAddress)
355 } catch (exception: UnknownHostException) {
360 val ipAddressesLabel = activity.getString(R.string.ip_addresses) + " "
362 // Create a spannable string builder.
363 val ipAddressesStringBuilder = SpannableStringBuilder(ipAddressesLabel + ipAddresses)
365 // Create a blue color span according to the theme.
366 val blueColorSpan = ForegroundColorSpan(activity.getColor(R.color.alt_blue_text))
368 // Set the string builder to display the certificate information in blue. `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
369 ipAddressesStringBuilder.setSpan(blueColorSpan, ipAddressesLabel.length, ipAddressesStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
371 // Return the formatted string.
372 return ipAddressesStringBuilder
375 // `onPostExecute()` operates on the UI thread.
376 @Deprecated("Deprecated in Java")
377 override fun onPostExecute(ipAddresses: SpannableStringBuilder) {
378 // Get handles for the activity and the alert dialog.
379 val activity = activityWeakReference.get()
380 val alertDialog = alertDialogWeakReference.get()
382 // Abort if the activity or the alert dialog is gone.
383 if (activity == null || activity.isFinishing || alertDialog == null) {
387 // Get a handle for the IP addresses text view.
388 val ipAddressesTextView = alertDialog.findViewById<TextView>(R.id.ip_addresses)!!
390 // Populate the IP addresses text view.
391 ipAddressesTextView.text = ipAddresses