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 dialogBuilder.setIconAttribute(R.attr.sslCertificateBlueIcon)
149 dialogBuilder.setTitle(R.string.ssl_certificate_error)
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))
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()
161 // Reset the SSL error handler.
162 nestedScrollWebView.resetSslErrorHandler()
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()
173 // Reset the SSL error handler.
174 nestedScrollWebView.resetSslErrorHandler()
178 // Create an alert dialog from the builder.
179 val alertDialog = dialogBuilder.create()
181 // Get a handle for the shared preferences.
182 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
184 // Get the screenshot preference.
185 val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
187 // Disable screenshots if not allowed.
188 if (!allowScreenshots) {
189 // Disable screenshots.
190 alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
193 // Get a URI for the URL with errors.
194 val uriWithErrors = Uri.parse(urlWithErrors)
196 // Get the IP addresses for the URI.
197 GetIpAddresses(requireActivity(), alertDialog).execute(uriWithErrors.host)
199 // The alert dialog must be shown before the contents can be modified.
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)!!
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) + " "
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)
235 // Define the color spans.
236 val blueColorSpan: ForegroundColorSpan
237 val redColorSpan: ForegroundColorSpan
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))
246 @Suppress("DEPRECATION")
247 blueColorSpan = ForegroundColorSpan(resources.getColor(R.color.violet_700))
248 @Suppress("DEPRECATION")
249 redColorSpan = ForegroundColorSpan(resources.getColor(R.color.red_900))
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)
263 // Define the primary error string.
264 var primaryErrorString = ""
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)
273 // Store the primary error string.
274 primaryErrorString = getString(R.string.cn_mismatch)
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))
283 @Suppress("DEPRECATION")
284 issuedByTextView.setTextColor(resources.getColor(R.color.red_900))
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)
292 // Store the primary error string.
293 primaryErrorString = getString(R.string.untrusted)
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))
302 @Suppress("DEPRECATION")
303 validDatesTextView.setTextColor(resources.getColor(R.color.red_900))
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)
310 // Store the primary error string.
311 primaryErrorString = getString(R.string.invalid_date)
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)
318 // Store the primary error string.
319 primaryErrorString = getString(R.string.future_certificate)
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)
326 // Store the primary error string.
327 primaryErrorString = getString(R.string.expired_certificate)
330 SslError.SSL_INVALID ->
331 // Store the primary error string.
332 primaryErrorString = getString(R.string.invalid_certificate)
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
347 // Return the alert dialog.
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)
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()
362 // Abort if the activity or the dialog is gone.
363 if (activity == null || activity.isFinishing || alertDialog == null) {
364 return SpannableStringBuilder()
367 // Initialize an IP address string builder.
368 val ipAddresses = StringBuilder()
370 // Get an array with the IP addresses for the host.
372 // Get an array with all the IP addresses for the domain.
373 val inetAddressesArray = InetAddress.getAllByName(domainName[0])
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")
383 // Add the IP Address to the string builder.
384 ipAddresses.append(inetAddress.hostAddress)
386 } catch (exception: UnknownHostException) {
391 val ipAddressesLabel = activity.getString(R.string.ip_addresses) + " "
393 // Create a spannable string builder.
394 val ipAddressesStringBuilder = SpannableStringBuilder(ipAddressesLabel + ipAddresses)
396 // Create a blue foreground color span.
397 val blueColorSpan: ForegroundColorSpan
399 // Get the current theme status.
400 val currentThemeStatus = activity.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
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))
407 @Suppress("DEPRECATION")
408 ForegroundColorSpan(activity.resources.getColor(R.color.violet_500))
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)
414 // Return the formatted string.
415 return ipAddressesStringBuilder
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()
424 // Abort if the activity or the alert dialog is gone.
425 if (activity == null || activity.isFinishing || alertDialog == null) {
429 // Get a handle for the IP addresses text view.
430 val ipAddressesTextView = alertDialog.findViewById<TextView>(R.id.ip_addresses)!!
432 // Populate the IP addresses text view.
433 ipAddressesTextView.text = ipAddresses