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