2 * Copyright 2016-2023 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.Dialog
23 import android.content.DialogInterface
24 import android.net.Uri
25 import android.net.http.SslError
26 import android.os.Bundle
27 import android.text.SpannableStringBuilder
28 import android.text.Spanned
29 import android.text.style.ForegroundColorSpan
30 import android.view.WindowManager
31 import android.webkit.SslErrorHandler
32 import android.widget.TextView
34 import androidx.appcompat.app.AlertDialog
35 import androidx.fragment.app.DialogFragment
36 import androidx.preference.PreferenceManager
38 import com.stoutner.privacybrowser.R
39 import com.stoutner.privacybrowser.activities.MainWebViewActivity
40 import com.stoutner.privacybrowser.coroutines.GetHostIpAddressesCoroutine
41 import com.stoutner.privacybrowser.views.NestedScrollWebView
43 import java.text.DateFormat
45 // Define the class constants.
46 private const val PRIMARY_ERROR_INT = "primary_error_int"
47 private const val URL_WITH_ERRORS = "url_with_errors"
48 private const val ISSUED_TO_CNAME = "issued_to_cname"
49 private const val ISSUED_TO_ONAME = "issued_to_oname"
50 private const val ISSUED_TO_UNAME = "issued_to_uname"
51 private const val ISSUED_BY_CNAME = "issued_by_cname"
52 private const val ISSUED_BY_ONAME = "issued_by_oname"
53 private const val ISSUED_BY_UNAME = "issued_by_uname"
54 private const val START_DATE = "start_date"
55 private const val END_DATE = "end_date"
56 private const val WEBVIEW_FRAGMENT_ID = "webview_fragment_id"
58 class SslCertificateErrorDialog : DialogFragment() {
60 fun displayDialog(sslError: SslError, webViewFragmentId: Long): SslCertificateErrorDialog {
61 // Get the various components of the SSL error message.
62 val primaryErrorInt = sslError.primaryError
63 val urlWithErrors = sslError.url
64 val sslCertificate = sslError.certificate
65 val issuedToCName = sslCertificate.issuedTo.cName
66 val issuedToOName = sslCertificate.issuedTo.oName
67 val issuedToUName = sslCertificate.issuedTo.uName
68 val issuedByCName = sslCertificate.issuedBy.cName
69 val issuedByOName = sslCertificate.issuedBy.oName
70 val issuedByUName = sslCertificate.issuedBy.uName
71 val startDate = sslCertificate.validNotBeforeDate
72 val endDate = sslCertificate.validNotAfterDate
74 // Create an arguments bundle.
75 val argumentsBundle = Bundle()
77 // Store the SSL error message components in the bundle.
78 argumentsBundle.putInt(PRIMARY_ERROR_INT, primaryErrorInt)
79 argumentsBundle.putString(URL_WITH_ERRORS, urlWithErrors)
80 argumentsBundle.putString(ISSUED_TO_CNAME, issuedToCName)
81 argumentsBundle.putString(ISSUED_TO_ONAME, issuedToOName)
82 argumentsBundle.putString(ISSUED_TO_UNAME, issuedToUName)
83 argumentsBundle.putString(ISSUED_BY_CNAME, issuedByCName)
84 argumentsBundle.putString(ISSUED_BY_ONAME, issuedByOName)
85 argumentsBundle.putString(ISSUED_BY_UNAME, issuedByUName)
86 argumentsBundle.putString(START_DATE, DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(startDate))
87 argumentsBundle.putString(END_DATE, DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(endDate))
88 argumentsBundle.putLong(WEBVIEW_FRAGMENT_ID, webViewFragmentId)
90 // Create a new instance of the SSL certificate error dialog.
91 val thisSslCertificateErrorDialog = SslCertificateErrorDialog()
93 // Add the arguments bundle to the new dialog.
94 thisSslCertificateErrorDialog.arguments = argumentsBundle
96 // Return the new dialog.
97 return thisSslCertificateErrorDialog
101 // Define the class variables.
102 private var sslErrorHandler: SslErrorHandler? = null
104 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
106 // Get the variables from the bundle.
107 val primaryErrorInt = requireArguments().getInt(PRIMARY_ERROR_INT)
108 val urlWithErrors = requireArguments().getString(URL_WITH_ERRORS)
109 val issuedToCName = requireArguments().getString(ISSUED_TO_CNAME)
110 val issuedToOName = requireArguments().getString(ISSUED_TO_ONAME)
111 val issuedToUName = requireArguments().getString(ISSUED_TO_UNAME)
112 val issuedByCName = requireArguments().getString(ISSUED_BY_CNAME)
113 val issuedByOName = requireArguments().getString(ISSUED_BY_ONAME)
114 val issuedByUName = requireArguments().getString(ISSUED_BY_UNAME)
115 val startDate = requireArguments().getString(START_DATE)
116 val endDate = requireArguments().getString(END_DATE)
117 val webViewFragmentId = requireArguments().getLong(WEBVIEW_FRAGMENT_ID)
119 // Get the current position of this WebView fragment.
120 val webViewPosition = MainWebViewActivity.webViewStateAdapter!!.getPositionForId(webViewFragmentId)
122 // Get the WebView tab fragment.
123 val webViewTabFragment = MainWebViewActivity.webViewStateAdapter!!.getPageFragment(webViewPosition)
125 // Get the fragment view.
126 val fragmentView = webViewTabFragment.requireView()
128 // Get a handle for the current WebView.
129 val nestedScrollWebView: NestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview)
131 // Get a handle for the SSL error handler.
132 sslErrorHandler = nestedScrollWebView.sslErrorHandler
134 // Use an alert dialog builder to create the alert dialog.
135 val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
138 dialogBuilder.setIcon(R.drawable.ssl_certificate)
141 dialogBuilder.setTitle(R.string.ssl_certificate_error)
144 dialogBuilder.setView(R.layout.ssl_certificate_error)
146 // Set the cancel button listener.
147 dialogBuilder.setNegativeButton(R.string.cancel) { _: DialogInterface?, _: Int ->
148 // Check to make sure the SSL error handler is not null. This might happen if multiple dialogs are displayed at once.
149 if (sslErrorHandler != null) {
150 // Cancel the request.
151 sslErrorHandler!!.cancel()
153 // Reset the SSL error handler.
154 nestedScrollWebView.resetSslErrorHandler()
158 // Set the proceed button listener.
159 dialogBuilder.setPositiveButton(R.string.proceed) { _: 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 // Proceed to the website.
163 sslErrorHandler!!.proceed()
165 // Reset the SSL error handler.
166 nestedScrollWebView.resetSslErrorHandler()
170 // Create an alert dialog from the builder.
171 val alertDialog = dialogBuilder.create()
173 // Get a handle for the shared preferences.
174 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
176 // Get the screenshot preference.
177 val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
179 // Disable screenshots if not allowed.
180 if (!allowScreenshots) {
181 // Disable screenshots.
182 alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
185 // Get a URI for the URL with errors.
186 val uriWithErrors = Uri.parse(urlWithErrors)
188 // The alert dialog must be shown before the contents can be modified.
191 // Get handles for the views.
192 val primaryErrorTextView = alertDialog.findViewById<TextView>(R.id.primary_error)!!
193 val urlTextView = alertDialog.findViewById<TextView>(R.id.url)!!
194 val ipAddressesTextView = alertDialog.findViewById<TextView>(R.id.ip_addresses)!!
195 val issuedToCNameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_cname)!!
196 val issuedToONameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_oname)!!
197 val issuedToUNameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_uname)!!
198 val issuedByTextView = alertDialog.findViewById<TextView>(R.id.issued_by_textview)!!
199 val issuedByCNameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_cname)!!
200 val issuedByONameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_oname)!!
201 val issuedByUNameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_uname)!!
202 val validDatesTextView = alertDialog.findViewById<TextView>(R.id.valid_dates_textview)!!
203 val startDateTextView = alertDialog.findViewById<TextView>(R.id.start_date)!!
204 val endDateTextView = alertDialog.findViewById<TextView>(R.id.end_date)!!
206 // Define the color spans.
207 val blueColorSpan = ForegroundColorSpan(requireContext().getColor(R.color.alt_blue_text))
208 val redColorSpan = ForegroundColorSpan(requireContext().getColor(R.color.red_text))
210 // Get the IP Addresses for the URI.
211 GetHostIpAddressesCoroutine.getAddresses(uriWithErrors.host!!, getString(R.string.ip_addresses), blueColorSpan, ipAddressesTextView)
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 // 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.
317 } catch (exception: Exception) { // The app was restarted while the dialog was displayed.
318 // Dismiss this new instance of the dialog as soon as it is displayed.
321 // Use an alert dialog builder to create an empty alert dialog.
322 val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
324 // Return the empty alert dialog.
325 return dialogBuilder.create()
329 override fun onSaveInstanceState(outState: Bundle) {
330 // Run the default commands.
331 super.onSaveInstanceState(outState)
333 // Cancel the request if the SSL error handler is not null. This resets the WebView so it is not waiting on a response to the error handler if it is restarted in the background.
334 // Otherwise, after restart, the dialog is no longer displayed, but the error handler is still pending and there is no way to cause the dialog to redisplay for that URL in that tab.
335 if (sslErrorHandler != null)
336 sslErrorHandler!!.cancel()