2 * Copyright © 2016-2021 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.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
37 import androidx.appcompat.app.AlertDialog
38 import androidx.fragment.app.DialogFragment
39 import androidx.preference.PreferenceManager
41 import com.stoutner.privacybrowser.R
42 import com.stoutner.privacybrowser.activities.MainWebViewActivity
43 import com.stoutner.privacybrowser.views.NestedScrollWebView
45 import java.lang.ref.WeakReference
46 import java.net.InetAddress
47 import java.net.UnknownHostException
48 import java.text.DateFormat
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"
63 class SslCertificateErrorDialog : DialogFragment() {
65 // `@JvmStatic` will no longer be required once all the code has transitioned to Kotlin.
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
81 // Create an arguments bundle.
82 val argumentsBundle = Bundle()
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)
97 // Create a new instance of the SSL certificate error dialog.
98 val thisSslCertificateErrorDialog = SslCertificateErrorDialog()
100 // Add the arguments bundle to the new dialog.
101 thisSslCertificateErrorDialog.arguments = argumentsBundle
103 // Return the new dialog.
104 return thisSslCertificateErrorDialog
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)
124 // Get the current position of this WebView fragment.
125 val webViewPosition = MainWebViewActivity.webViewPagerAdapter.getPositionForId(webViewFragmentId)
127 // Get the WebView tab fragment.
128 val webViewTabFragment = MainWebViewActivity.webViewPagerAdapter.getPageFragment(webViewPosition)
130 // Get the fragment view.
131 val fragmentView = webViewTabFragment.requireView()
133 // Get a handle for the current WebView.
134 val nestedScrollWebView: NestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview)
136 // Get a handle for the SSL error handler.
137 val sslErrorHandler = nestedScrollWebView.sslErrorHandler
139 // Use an alert dialog builder to create the alert dialog.
140 val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
142 // Get the current theme status.
143 val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
145 // Set the icon according to the theme.
146 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
147 dialogBuilder.setIcon(R.drawable.ssl_certificate_enabled_day)
149 dialogBuilder.setIcon(R.drawable.ssl_certificate_enabled_night)
153 dialogBuilder.setTitle(R.string.ssl_certificate_error)
155 // Set the view. The parent view is null because it will be assigned by the alert dialog.
156 dialogBuilder.setView(layoutInflater.inflate(R.layout.ssl_certificate_error, null))
158 // Set a listener on the cancel button.
159 dialogBuilder.setNegativeButton(R.string.cancel) { _: DialogInterface?, _: Int ->
160 // Check to make sure the SSL error handler is not null. This might happen if multiple dialogs are displayed at once.
161 if (sslErrorHandler != null) {
162 // Cancel the request.
163 sslErrorHandler.cancel()
165 // Reset the SSL error handler.
166 nestedScrollWebView.resetSslErrorHandler()
170 // Set a listener on the proceed button.
171 dialogBuilder.setPositiveButton(R.string.proceed) { _: DialogInterface?, _: Int ->
172 // Check to make sure the SSL error handler is not null. This might happen if multiple dialogs are displayed at once.
173 if (sslErrorHandler != null) {
174 // Proceed to the website.
175 sslErrorHandler.proceed()
177 // Reset the SSL error handler.
178 nestedScrollWebView.resetSslErrorHandler()
182 // Create an alert dialog from the builder.
183 val alertDialog = dialogBuilder.create()
185 // Get a handle for the shared preferences.
186 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
188 // Get the screenshot preference.
189 val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
191 // Disable screenshots if not allowed.
192 if (!allowScreenshots) {
193 // Disable screenshots.
194 alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
197 // Get a URI for the URL with errors.
198 val uriWithErrors = Uri.parse(urlWithErrors)
200 // Get the IP addresses for the URI.
201 GetIpAddresses(requireActivity(), alertDialog).execute(uriWithErrors.host)
203 // The alert dialog must be shown before the contents can be modified.
206 // Get handles for the views.
207 val primaryErrorTextView = alertDialog.findViewById<TextView>(R.id.primary_error)!!
208 val urlTextView = alertDialog.findViewById<TextView>(R.id.url)!!
209 val issuedToCNameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_cname)!!
210 val issuedToONameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_oname)!!
211 val issuedToUNameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_uname)!!
212 val issuedByTextView = alertDialog.findViewById<TextView>(R.id.issued_by_textview)!!
213 val issuedByCNameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_cname)!!
214 val issuedByONameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_oname)!!
215 val issuedByUNameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_uname)!!
216 val validDatesTextView = alertDialog.findViewById<TextView>(R.id.valid_dates_textview)!!
217 val startDateTextView = alertDialog.findViewById<TextView>(R.id.start_date)!!
218 val endDateTextView = alertDialog.findViewById<TextView>(R.id.end_date)!!
220 // Setup the common strings.
221 val urlLabel = getString(R.string.url_label) + " "
222 val cNameLabel = getString(R.string.common_name) + " "
223 val oNameLabel = getString(R.string.organization) + " "
224 val uNameLabel = getString(R.string.organizational_unit) + " "
225 val startDateLabel = getString(R.string.start_date) + " "
226 val endDateLabel = getString(R.string.end_date) + " "
228 // Create a spannable string builder for each text view that needs multiple colors of text.
229 val urlStringBuilder = SpannableStringBuilder(urlLabel + urlWithErrors)
230 val issuedToCNameStringBuilder = SpannableStringBuilder(cNameLabel + issuedToCName)
231 val issuedToONameStringBuilder = SpannableStringBuilder(oNameLabel + issuedToOName)
232 val issuedToUNameStringBuilder = SpannableStringBuilder(uNameLabel + issuedToUName)
233 val issuedByCNameStringBuilder = SpannableStringBuilder(cNameLabel + issuedByCName)
234 val issuedByONameStringBuilder = SpannableStringBuilder(oNameLabel + issuedByOName)
235 val issuedByUNameStringBuilder = SpannableStringBuilder(uNameLabel + issuedByUName)
236 val startDateStringBuilder = SpannableStringBuilder(startDateLabel + startDate)
237 val endDateStringBuilder = SpannableStringBuilder(endDateLabel + endDate)
239 // Define the color spans.
240 val blueColorSpan: ForegroundColorSpan
241 val redColorSpan: ForegroundColorSpan
243 // Set the color spans according to the theme. The deprecated `getColor()` must be used until the minimum API >= 23.
244 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
245 @Suppress("DEPRECATION")
246 blueColorSpan = ForegroundColorSpan(resources.getColor(R.color.blue_700))
247 @Suppress("DEPRECATION")
248 redColorSpan = ForegroundColorSpan(resources.getColor(R.color.red_a700))
250 @Suppress("DEPRECATION")
251 blueColorSpan = ForegroundColorSpan(resources.getColor(R.color.violet_700))
252 @Suppress("DEPRECATION")
253 redColorSpan = ForegroundColorSpan(resources.getColor(R.color.red_900))
256 // Setup the spans to display the certificate information in blue. `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
257 urlStringBuilder.setSpan(blueColorSpan, urlLabel.length, urlStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
258 issuedToCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
259 issuedToONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length, issuedToONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
260 issuedToUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length, issuedToUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
261 issuedByCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedByCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
262 issuedByONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length, issuedByONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
263 issuedByUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length, issuedByUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
264 startDateStringBuilder.setSpan(blueColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
265 endDateStringBuilder.setSpan(blueColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
267 // Define the primary error string.
268 var primaryErrorString = ""
270 // Highlight the primary error in red and store it in the primary error string.
271 when (primaryErrorInt) {
272 SslError.SSL_IDMISMATCH -> {
273 // Change the URL span colors to red.
274 urlStringBuilder.setSpan(redColorSpan, urlLabel.length, urlStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
275 issuedToCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
277 // Store the primary error string.
278 primaryErrorString = getString(R.string.cn_mismatch)
281 SslError.SSL_UNTRUSTED -> {
282 // Change the issued by text view text to red. The deprecated `getColor()` must be used until the minimum API >= 23.
283 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
284 @Suppress("DEPRECATION")
285 issuedByTextView.setTextColor(resources.getColor(R.color.red_a700))
287 @Suppress("DEPRECATION")
288 issuedByTextView.setTextColor(resources.getColor(R.color.red_900))
291 // Change the issued by span color to red.
292 issuedByCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length, issuedByCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
293 issuedByONameStringBuilder.setSpan(redColorSpan, oNameLabel.length, issuedByONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
294 issuedByUNameStringBuilder.setSpan(redColorSpan, uNameLabel.length, issuedByUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
296 // Store the primary error string.
297 primaryErrorString = getString(R.string.untrusted)
300 SslError.SSL_DATE_INVALID -> {
301 // Change the valid dates text view text to red. The deprecated `getColor()` must be used until the minimum API >= 23.
302 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
303 @Suppress("DEPRECATION")
304 validDatesTextView.setTextColor(resources.getColor(R.color.red_a700))
306 @Suppress("DEPRECATION")
307 validDatesTextView.setTextColor(resources.getColor(R.color.red_900))
310 // Change the date span colors to red.
311 startDateStringBuilder.setSpan(redColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
312 endDateStringBuilder.setSpan(redColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
314 // Store the primary error string.
315 primaryErrorString = getString(R.string.invalid_date)
318 SslError.SSL_NOTYETVALID -> {
319 // Change the start date span color to red.
320 startDateStringBuilder.setSpan(redColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
322 // Store the primary error string.
323 primaryErrorString = getString(R.string.future_certificate)
326 SslError.SSL_EXPIRED -> {
327 // Change the end date span color to red.
328 endDateStringBuilder.setSpan(redColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
330 // Store the primary error string.
331 primaryErrorString = getString(R.string.expired_certificate)
334 SslError.SSL_INVALID ->
335 // Store the primary error string.
336 primaryErrorString = getString(R.string.invalid_certificate)
340 // Display the strings.
341 primaryErrorTextView.text = primaryErrorString
342 urlTextView.text = urlStringBuilder
343 issuedToCNameTextView.text = issuedToCNameStringBuilder
344 issuedToONameTextView.text = issuedToONameStringBuilder
345 issuedToUNameTextView.text = issuedToUNameStringBuilder
346 issuedByCNameTextView.text = issuedByCNameStringBuilder
347 issuedByONameTextView.text = issuedByONameStringBuilder
348 issuedByUNameTextView.text = issuedByUNameStringBuilder
349 startDateTextView.text = startDateStringBuilder
350 endDateTextView.text = endDateStringBuilder
352 // Return the alert dialog.
356 // This must run asynchronously because it involves a network request. `String` declares the parameters. `Void` does not declare progress units. `SpannableStringBuilder` contains the results.
357 private class GetIpAddresses constructor(activity: Activity, alertDialog: AlertDialog) : AsyncTask<String, Void?, SpannableStringBuilder>() {
358 // Define the weak references.
359 private val activityWeakReference: WeakReference<Activity> = WeakReference(activity)
360 private val alertDialogWeakReference: WeakReference<AlertDialog> = WeakReference(alertDialog)
362 override fun doInBackground(vararg domainName: String): SpannableStringBuilder {
363 // Get handles for the activity and the alert dialog.
364 val activity = activityWeakReference.get()
365 val alertDialog = alertDialogWeakReference.get()
367 // Abort if the activity or the dialog is gone.
368 if (activity == null || activity.isFinishing || alertDialog == null) {
369 return SpannableStringBuilder()
372 // Initialize an IP address string builder.
373 val ipAddresses = StringBuilder()
375 // Get an array with the IP addresses for the host.
377 // Get an array with all the IP addresses for the domain.
378 val inetAddressesArray = InetAddress.getAllByName(domainName[0])
380 // Add each IP address to the string builder.
381 for (inetAddress in inetAddressesArray) {
382 // Check to see if this is not the first IP address.
383 if (ipAddresses.isNotEmpty()) {
384 // Add a line break to the string builder first.
385 ipAddresses.append("\n")
388 // Add the IP Address to the string builder.
389 ipAddresses.append(inetAddress.hostAddress)
391 } catch (exception: UnknownHostException) {
396 val ipAddressesLabel = activity.getString(R.string.ip_addresses) + " "
398 // Create a spannable string builder.
399 val ipAddressesStringBuilder = SpannableStringBuilder(ipAddressesLabel + ipAddresses)
401 // Create a blue foreground color span.
402 val blueColorSpan: ForegroundColorSpan
404 // Get the current theme status.
405 val currentThemeStatus = activity.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
407 // Set the blue color span according to the theme. The deprecated `getColor()` must be used until the minimum API >= 23.
408 blueColorSpan = if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
409 @Suppress("DEPRECATION")
410 ForegroundColorSpan(activity.resources.getColor(R.color.blue_700))
412 @Suppress("DEPRECATION")
413 ForegroundColorSpan(activity.resources.getColor(R.color.violet_500))
416 // Set the string builder to display the certificate information in blue. `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
417 ipAddressesStringBuilder.setSpan(blueColorSpan, ipAddressesLabel.length, ipAddressesStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
419 // Return the formatted string.
420 return ipAddressesStringBuilder
423 // `onPostExecute()` operates on the UI thread.
424 override fun onPostExecute(ipAddresses: SpannableStringBuilder) {
425 // Get handles for the activity and the alert dialog.
426 val activity = activityWeakReference.get()
427 val alertDialog = alertDialogWeakReference.get()
429 // Abort if the activity or the alert dialog is gone.
430 if (activity == null || activity.isFinishing || alertDialog == null) {
434 // Get a handle for the IP addresses text view.
435 val ipAddressesTextView = alertDialog.findViewById<TextView>(R.id.ip_addresses)!!
437 // Populate the IP addresses text view.
438 ipAddressesTextView.text = ipAddresses