X-Git-Url: https://gitweb.stoutner.com/?p=PrivacyBrowserAndroid.git;a=blobdiff_plain;f=app%2Fsrc%2Fmain%2Fjava%2Fcom%2Fstoutner%2Fprivacybrowser%2Fdialogs%2FSslCertificateErrorDialog.kt;fp=app%2Fsrc%2Fmain%2Fjava%2Fcom%2Fstoutner%2Fprivacybrowser%2Fdialogs%2FSslCertificateErrorDialog.kt;h=139e6576e78b7055bfac1fa4786a2706f8bead0a;hp=0000000000000000000000000000000000000000;hb=031def95c6d9bfc14113fe86b4a5690233d93ce2;hpb=f3b9172adedd74f705ddc0beac80798ae84f2920 diff --git a/app/src/main/java/com/stoutner/privacybrowser/dialogs/SslCertificateErrorDialog.kt b/app/src/main/java/com/stoutner/privacybrowser/dialogs/SslCertificateErrorDialog.kt new file mode 100644 index 00000000..139e6576 --- /dev/null +++ b/app/src/main/java/com/stoutner/privacybrowser/dialogs/SslCertificateErrorDialog.kt @@ -0,0 +1,441 @@ +/* + * Copyright © 2016-2021 Soren Stoutner . + * + * This file is part of Privacy Browser . + * + * Privacy Browser is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Privacy Browser is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Privacy Browser. If not, see . + */ + +package com.stoutner.privacybrowser.dialogs + +import android.annotation.SuppressLint +import android.app.Activity +import android.app.Dialog +import android.content.DialogInterface +import android.content.res.Configuration +import android.net.Uri +import android.net.http.SslError +import android.os.AsyncTask +import android.os.Bundle +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.style.ForegroundColorSpan +import android.view.WindowManager +import android.widget.TextView + +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import androidx.preference.PreferenceManager + +import com.stoutner.privacybrowser.R +import com.stoutner.privacybrowser.activities.MainWebViewActivity +import com.stoutner.privacybrowser.views.NestedScrollWebView + +import java.lang.ref.WeakReference +import java.net.InetAddress +import java.net.UnknownHostException +import java.text.DateFormat + +// Define the class constants. +private const val PRIMARY_ERROR_INT = "primary_error_int" +private const val URL_WITH_ERRORS = "url_with_errors" +private const val ISSUED_TO_CNAME = "issued_to_cname" +private const val ISSUED_TO_ONAME = "issued_to_oname" +private const val ISSUED_TO_UNAME = "issued_to_uname" +private const val ISSUED_BY_CNAME = "issued_by_cname" +private const val ISSUED_BY_ONAME = "issued_by_oname" +private const val ISSUED_BY_UNAME = "issued_by_uname" +private const val START_DATE = "start_date" +private const val END_DATE = "end_date" +private const val WEBVIEW_FRAGMENT_ID = "webview_fragment_id" + +class SslCertificateErrorDialog : DialogFragment() { + companion object { + // `@JvmStatic` will no longer be required once all the code has transitioned to Kotlin. + @JvmStatic + fun displayDialog(sslError: SslError, webViewFragmentId: Long): SslCertificateErrorDialog { + // Get the various components of the SSL error message. + val primaryErrorInt = sslError.primaryError + val urlWithErrors = sslError.url + val sslCertificate = sslError.certificate + val issuedToCName = sslCertificate.issuedTo.cName + val issuedToOName = sslCertificate.issuedTo.oName + val issuedToUName = sslCertificate.issuedTo.uName + val issuedByCName = sslCertificate.issuedBy.cName + val issuedByOName = sslCertificate.issuedBy.oName + val issuedByUName = sslCertificate.issuedBy.uName + val startDate = sslCertificate.validNotBeforeDate + val endDate = sslCertificate.validNotAfterDate + + // Create an arguments bundle. + val argumentsBundle = Bundle() + + // Store the SSL error message components in the bundle. + argumentsBundle.putInt(PRIMARY_ERROR_INT, primaryErrorInt) + argumentsBundle.putString(URL_WITH_ERRORS, urlWithErrors) + argumentsBundle.putString(ISSUED_TO_CNAME, issuedToCName) + argumentsBundle.putString(ISSUED_TO_ONAME, issuedToOName) + argumentsBundle.putString(ISSUED_TO_UNAME, issuedToUName) + argumentsBundle.putString(ISSUED_BY_CNAME, issuedByCName) + argumentsBundle.putString(ISSUED_BY_ONAME, issuedByOName) + argumentsBundle.putString(ISSUED_BY_UNAME, issuedByUName) + argumentsBundle.putString(START_DATE, DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(startDate)) + argumentsBundle.putString(END_DATE, DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(endDate)) + argumentsBundle.putLong(WEBVIEW_FRAGMENT_ID, webViewFragmentId) + + // Create a new instance of the SSL certificate error dialog. + val thisSslCertificateErrorDialog = SslCertificateErrorDialog() + + // Add the arguments bundle to the new dialog. + thisSslCertificateErrorDialog.arguments = argumentsBundle + + // Return the new dialog. + return thisSslCertificateErrorDialog + } + } + + // `@SuppressLint("InflateParams")` removes the warning about using `null` as the parent view group when inflating the alert dialog. + @SuppressLint("InflateParams") + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + // Get the variables from the bundle. + val primaryErrorInt = requireArguments().getInt(PRIMARY_ERROR_INT) + val urlWithErrors = requireArguments().getString(URL_WITH_ERRORS) + val issuedToCName = requireArguments().getString(ISSUED_TO_CNAME) + val issuedToOName = requireArguments().getString(ISSUED_TO_ONAME) + val issuedToUName = requireArguments().getString(ISSUED_TO_UNAME) + val issuedByCName = requireArguments().getString(ISSUED_BY_CNAME) + val issuedByOName = requireArguments().getString(ISSUED_BY_ONAME) + val issuedByUName = requireArguments().getString(ISSUED_BY_UNAME) + val startDate = requireArguments().getString(START_DATE) + val endDate = requireArguments().getString(END_DATE) + val webViewFragmentId = requireArguments().getLong(WEBVIEW_FRAGMENT_ID) + + // Get the current position of this WebView fragment. + val webViewPosition = MainWebViewActivity.webViewPagerAdapter.getPositionForId(webViewFragmentId) + + // Get the WebView tab fragment. + val webViewTabFragment = MainWebViewActivity.webViewPagerAdapter.getPageFragment(webViewPosition) + + // Get the fragment view. + val fragmentView = webViewTabFragment.requireView() + + // Get a handle for the current WebView. + val nestedScrollWebView: NestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview) + + // Get a handle for the SSL error handler. + val sslErrorHandler = nestedScrollWebView.sslErrorHandler + + // Use an alert dialog builder to create the alert dialog. + val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog) + + // Get the current theme status. + val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + + // Set the icon according to the theme. + if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { + dialogBuilder.setIcon(R.drawable.ssl_certificate_enabled_day) + } else { + dialogBuilder.setIcon(R.drawable.ssl_certificate_enabled_night) + } + + // Set the title. + dialogBuilder.setTitle(R.string.ssl_certificate_error) + + // Set the view. The parent view is null because it will be assigned by the alert dialog. + dialogBuilder.setView(layoutInflater.inflate(R.layout.ssl_certificate_error, null)) + + // Set a listener on the cancel button. + dialogBuilder.setNegativeButton(R.string.cancel) { _: DialogInterface?, _: Int -> + // Check to make sure the SSL error handler is not null. This might happen if multiple dialogs are displayed at once. + if (sslErrorHandler != null) { + // Cancel the request. + sslErrorHandler.cancel() + + // Reset the SSL error handler. + nestedScrollWebView.resetSslErrorHandler() + } + } + + // Set a listener on the proceed button. + dialogBuilder.setPositiveButton(R.string.proceed) { _: DialogInterface?, _: Int -> + // Check to make sure the SSL error handler is not null. This might happen if multiple dialogs are displayed at once. + if (sslErrorHandler != null) { + // Proceed to the website. + sslErrorHandler.proceed() + + // Reset the SSL error handler. + nestedScrollWebView.resetSslErrorHandler() + } + } + + // Create an alert dialog from the builder. + val alertDialog = dialogBuilder.create() + + // Get a handle for the shared preferences. + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) + + // Get the screenshot preference. + val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false) + + // Disable screenshots if not allowed. + if (!allowScreenshots) { + // Disable screenshots. + alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE) + } + + // Get a URI for the URL with errors. + val uriWithErrors = Uri.parse(urlWithErrors) + + // Get the IP addresses for the URI. + GetIpAddresses(requireActivity(), alertDialog).execute(uriWithErrors.host) + + // The alert dialog must be shown before the contents can be modified. + alertDialog.show() + + // Get handles for the views. + val primaryErrorTextView = alertDialog.findViewById(R.id.primary_error)!! + val urlTextView = alertDialog.findViewById(R.id.url)!! + val issuedToCNameTextView = alertDialog.findViewById(R.id.issued_to_cname)!! + val issuedToONameTextView = alertDialog.findViewById(R.id.issued_to_oname)!! + val issuedToUNameTextView = alertDialog.findViewById(R.id.issued_to_uname)!! + val issuedByTextView = alertDialog.findViewById(R.id.issued_by_textview)!! + val issuedByCNameTextView = alertDialog.findViewById(R.id.issued_by_cname)!! + val issuedByONameTextView = alertDialog.findViewById(R.id.issued_by_oname)!! + val issuedByUNameTextView = alertDialog.findViewById(R.id.issued_by_uname)!! + val validDatesTextView = alertDialog.findViewById(R.id.valid_dates_textview)!! + val startDateTextView = alertDialog.findViewById(R.id.start_date)!! + val endDateTextView = alertDialog.findViewById(R.id.end_date)!! + + // Setup the common strings. + val urlLabel = getString(R.string.url_label) + " " + val cNameLabel = getString(R.string.common_name) + " " + val oNameLabel = getString(R.string.organization) + " " + val uNameLabel = getString(R.string.organizational_unit) + " " + val startDateLabel = getString(R.string.start_date) + " " + val endDateLabel = getString(R.string.end_date) + " " + + // Create a spannable string builder for each text view that needs multiple colors of text. + val urlStringBuilder = SpannableStringBuilder(urlLabel + urlWithErrors) + val issuedToCNameStringBuilder = SpannableStringBuilder(cNameLabel + issuedToCName) + val issuedToONameStringBuilder = SpannableStringBuilder(oNameLabel + issuedToOName) + val issuedToUNameStringBuilder = SpannableStringBuilder(uNameLabel + issuedToUName) + val issuedByCNameStringBuilder = SpannableStringBuilder(cNameLabel + issuedByCName) + val issuedByONameStringBuilder = SpannableStringBuilder(oNameLabel + issuedByOName) + val issuedByUNameStringBuilder = SpannableStringBuilder(uNameLabel + issuedByUName) + val startDateStringBuilder = SpannableStringBuilder(startDateLabel + startDate) + val endDateStringBuilder = SpannableStringBuilder(endDateLabel + endDate) + + // Define the color spans. + val blueColorSpan: ForegroundColorSpan + val redColorSpan: ForegroundColorSpan + + // Set the color spans according to the theme. The deprecated `getColor()` must be used until the minimum API >= 23. + if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { + @Suppress("DEPRECATION") + blueColorSpan = ForegroundColorSpan(resources.getColor(R.color.blue_700)) + @Suppress("DEPRECATION") + redColorSpan = ForegroundColorSpan(resources.getColor(R.color.red_a700)) + } else { + @Suppress("DEPRECATION") + blueColorSpan = ForegroundColorSpan(resources.getColor(R.color.violet_700)) + @Suppress("DEPRECATION") + redColorSpan = ForegroundColorSpan(resources.getColor(R.color.red_900)) + } + + // Setup the spans to display the certificate information in blue. `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction. + urlStringBuilder.setSpan(blueColorSpan, urlLabel.length, urlStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE) + issuedToCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE) + issuedToONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length, issuedToONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE) + issuedToUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length, issuedToUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE) + issuedByCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedByCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE) + issuedByONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length, issuedByONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE) + issuedByUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length, issuedByUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE) + startDateStringBuilder.setSpan(blueColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE) + endDateStringBuilder.setSpan(blueColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE) + + // Define the primary error string. + var primaryErrorString = "" + + // Highlight the primary error in red and store it in the primary error string. + when (primaryErrorInt) { + SslError.SSL_IDMISMATCH -> { + // Change the URL span colors to red. + urlStringBuilder.setSpan(redColorSpan, urlLabel.length, urlStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE) + issuedToCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE) + + // Store the primary error string. + primaryErrorString = getString(R.string.cn_mismatch) + } + + SslError.SSL_UNTRUSTED -> { + // Change the issued by text view text to red. The deprecated `getColor()` must be used until the minimum API >= 23. + if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { + @Suppress("DEPRECATION") + issuedByTextView.setTextColor(resources.getColor(R.color.red_a700)) + } else { + @Suppress("DEPRECATION") + issuedByTextView.setTextColor(resources.getColor(R.color.red_900)) + } + + // Change the issued by span color to red. + issuedByCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length, issuedByCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE) + issuedByONameStringBuilder.setSpan(redColorSpan, oNameLabel.length, issuedByONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE) + issuedByUNameStringBuilder.setSpan(redColorSpan, uNameLabel.length, issuedByUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE) + + // Store the primary error string. + primaryErrorString = getString(R.string.untrusted) + } + + SslError.SSL_DATE_INVALID -> { + // Change the valid dates text view text to red. The deprecated `getColor()` must be used until the minimum API >= 23. + if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { + @Suppress("DEPRECATION") + validDatesTextView.setTextColor(resources.getColor(R.color.red_a700)) + } else { + @Suppress("DEPRECATION") + validDatesTextView.setTextColor(resources.getColor(R.color.red_900)) + } + + // Change the date span colors to red. + startDateStringBuilder.setSpan(redColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE) + endDateStringBuilder.setSpan(redColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE) + + // Store the primary error string. + primaryErrorString = getString(R.string.invalid_date) + } + + SslError.SSL_NOTYETVALID -> { + // Change the start date span color to red. + startDateStringBuilder.setSpan(redColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE) + + // Store the primary error string. + primaryErrorString = getString(R.string.future_certificate) + } + + SslError.SSL_EXPIRED -> { + // Change the end date span color to red. + endDateStringBuilder.setSpan(redColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE) + + // Store the primary error string. + primaryErrorString = getString(R.string.expired_certificate) + } + + SslError.SSL_INVALID -> + // Store the primary error string. + primaryErrorString = getString(R.string.invalid_certificate) + } + + + // Display the strings. + primaryErrorTextView.text = primaryErrorString + urlTextView.text = urlStringBuilder + issuedToCNameTextView.text = issuedToCNameStringBuilder + issuedToONameTextView.text = issuedToONameStringBuilder + issuedToUNameTextView.text = issuedToUNameStringBuilder + issuedByCNameTextView.text = issuedByCNameStringBuilder + issuedByONameTextView.text = issuedByONameStringBuilder + issuedByUNameTextView.text = issuedByUNameStringBuilder + startDateTextView.text = startDateStringBuilder + endDateTextView.text = endDateStringBuilder + + // Return the alert dialog. + return alertDialog + } + + // This must run asynchronously because it involves a network request. `String` declares the parameters. `Void` does not declare progress units. `SpannableStringBuilder` contains the results. + private class GetIpAddresses constructor(activity: Activity, alertDialog: AlertDialog) : AsyncTask() { + // Define the weak references. + private val activityWeakReference: WeakReference = WeakReference(activity) + private val alertDialogWeakReference: WeakReference = WeakReference(alertDialog) + + override fun doInBackground(vararg domainName: String): SpannableStringBuilder { + // Get handles for the activity and the alert dialog. + val activity = activityWeakReference.get() + val alertDialog = alertDialogWeakReference.get() + + // Abort if the activity or the dialog is gone. + if (activity == null || activity.isFinishing || alertDialog == null) { + return SpannableStringBuilder() + } + + // Initialize an IP address string builder. + val ipAddresses = StringBuilder() + + // Get an array with the IP addresses for the host. + try { + // Get an array with all the IP addresses for the domain. + val inetAddressesArray = InetAddress.getAllByName(domainName[0]) + + // Add each IP address to the string builder. + for (inetAddress in inetAddressesArray) { + // Check to see if this is not the first IP address. + if (ipAddresses.isNotEmpty()) { + // Add a line break to the string builder first. + ipAddresses.append("\n") + } + + // Add the IP Address to the string builder. + ipAddresses.append(inetAddress.hostAddress) + } + } catch (exception: UnknownHostException) { + // Do nothing. + } + + // Set the label. + val ipAddressesLabel = activity.getString(R.string.ip_addresses) + " " + + // Create a spannable string builder. + val ipAddressesStringBuilder = SpannableStringBuilder(ipAddressesLabel + ipAddresses) + + // Create a blue foreground color span. + val blueColorSpan: ForegroundColorSpan + + // Get the current theme status. + val currentThemeStatus = activity.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + + // Set the blue color span according to the theme. The deprecated `getColor()` must be used until the minimum API >= 23. + blueColorSpan = if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) { + @Suppress("DEPRECATION") + ForegroundColorSpan(activity.resources.getColor(R.color.blue_700)) + } else { + @Suppress("DEPRECATION") + ForegroundColorSpan(activity.resources.getColor(R.color.violet_500)) + } + + // Set the string builder to display the certificate information in blue. `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction. + ipAddressesStringBuilder.setSpan(blueColorSpan, ipAddressesLabel.length, ipAddressesStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE) + + // Return the formatted string. + return ipAddressesStringBuilder + } + + // `onPostExecute()` operates on the UI thread. + override fun onPostExecute(ipAddresses: SpannableStringBuilder) { + // Get handles for the activity and the alert dialog. + val activity = activityWeakReference.get() + val alertDialog = alertDialogWeakReference.get() + + // Abort if the activity or the alert dialog is gone. + if (activity == null || activity.isFinishing || alertDialog == null) { + return + } + + // Get a handle for the IP addresses text view. + val ipAddressesTextView = alertDialog.findViewById(R.id.ip_addresses)!! + + // Populate the IP addresses text view. + ipAddressesTextView.text = ipAddresses + } + } +} \ No newline at end of file