From 09c5cb0a0d33c58897026b7b4b1901c1ddc088f1 Mon Sep 17 00:00:00 2001 From: Arne Schwabe Date: Tue, 24 Aug 2021 19:25:59 +0200 Subject: Implement support of openvpn://import-profile/ support For details about the protocol see https://github.com/OpenVPN/openvpn3/blob/master/doc/webauth.md --- .../de/blinkt/openvpn/core/VPNLaunchHelper.java | 2 +- main/src/main/res/layout/import_as_config.xml | 57 --- main/src/main/res/layout/import_remote_config.xml | 82 ++++ main/src/main/res/values/strings.xml | 7 +- main/src/ui/AndroidManifest.xml | 7 + .../de/blinkt/openvpn/activities/MainActivity.java | 34 +- .../de/blinkt/openvpn/fragments/ImportASConfig.kt | 421 ------------------- .../blinkt/openvpn/fragments/ImportRemoteConfig.kt | 465 +++++++++++++++++++++ .../blinkt/openvpn/fragments/VPNProfileList.java | 14 +- 9 files changed, 594 insertions(+), 495 deletions(-) delete mode 100644 main/src/main/res/layout/import_as_config.xml create mode 100644 main/src/main/res/layout/import_remote_config.xml delete mode 100644 main/src/ui/java/de/blinkt/openvpn/fragments/ImportASConfig.kt create mode 100644 main/src/ui/java/de/blinkt/openvpn/fragments/ImportRemoteConfig.kt diff --git a/main/src/main/java/de/blinkt/openvpn/core/VPNLaunchHelper.java b/main/src/main/java/de/blinkt/openvpn/core/VPNLaunchHelper.java index 7fb01032..52ec55d7 100644 --- a/main/src/main/java/de/blinkt/openvpn/core/VPNLaunchHelper.java +++ b/main/src/main/java/de/blinkt/openvpn/core/VPNLaunchHelper.java @@ -51,7 +51,7 @@ public class VPNLaunchHelper { } } - throw new RuntimeException("Cannot find any execulte for this device's ABIs " + Arrays.toString(abis)); + throw new RuntimeException("Cannot find any executable for this device's ABIs " + Arrays.toString(abis)); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) diff --git a/main/src/main/res/layout/import_as_config.xml b/main/src/main/res/layout/import_as_config.xml deleted file mode 100644 index d9651db8..00000000 --- a/main/src/main/res/layout/import_as_config.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/main/src/main/res/layout/import_remote_config.xml b/main/src/main/res/layout/import_remote_config.xml new file mode 100644 index 00000000..f04503ce --- /dev/null +++ b/main/src/main/res/layout/import_remote_config.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/main/src/main/res/values/strings.xml b/main/src/main/res/values/strings.xml index 4511d80c..7cab17e4 100755 --- a/main/src/main/res/values/strings.xml +++ b/main/src/main/res/values/strings.xml @@ -33,7 +33,7 @@ IPv4 Address IPv6 Address Enter custom OpenVPN options. Use with caution. Also note that many of the tun related OpenVPN settings cannot be supported by design of the VPNSettings. If you think an important option is missing contact the author - Username + Username (leave empty for no auth) Password For the static configuration the TLS Auth Keys will be used as static keys Configure the VPN @@ -494,8 +494,9 @@ Block IPv6 (or IPv4) if not used by the VPN Install new certificate AS servername + Server URL Request autologin profile - Import Profile from Access Server + Import Profile from Remote Server Default VPN not set. Please set the Default VPN before enabling this option. Internal WebView Failed to negotiate cipher with server @@ -506,5 +507,7 @@ Please you the Always-On Feature of Android to enable VPN at boot time. Open VPN Settings Press here open a window to enter additional required authentication + OpenVPN Access Server + URL diff --git a/main/src/ui/AndroidManifest.xml b/main/src/ui/AndroidManifest.xml index a9dfa380..0caccd5d 100644 --- a/main/src/ui/AndroidManifest.xml +++ b/main/src/ui/AndroidManifest.xml @@ -31,6 +31,13 @@ + + + + + + + (object : X509TrustManager { - override fun getAcceptedIssuers(): Array { - return emptyArray() - } - - @Throws(CertificateException::class) - override fun checkClientTrusted(chain: Array, authType: String) { - throw CertificateException("Why would we check client certificates?!") - } - - @Throws(CertificateException::class) - override fun checkServerTrusted(chain: Array, authType: String) { - certPin.check(hostname, chain.toList()) - } - }) - - // Install the all-trusting trust manager - val sslContext = SSLContext.getInstance("TLS") - sslContext.init(null, trustPinnedCerts, java.security.SecureRandom()) - // Create an ssl socket factory with our all-trusting manager - - return sslContext.socketFactory - -} - -class ImportASConfig : DialogFragment() { - private lateinit var asUseAutlogin: CheckBox - private lateinit var asServername: EditText - private lateinit var asUsername: EditText - private lateinit var asPassword: EditText - private lateinit var dialogView: View - - - - internal fun getHostNameVerifier(prefs: SharedPreferences): HostnameVerifier { - val pinnedHostnames: Set = prefs.getStringSet("pinnedHosts", emptySet())!! - - val mapping = mutableMapOf() - - pinnedHostnames.forEach { ph -> - mapping[ph] = prefs.getString("pin-${ph}", "") - } - - val defaultVerifier = OkHostnameVerifier.INSTANCE; - val pinHostVerifier = object : HostnameVerifier { - override fun verify(hostname: String?, session: SSLSession?): Boolean { - val unverifiedHandshake = Handshake.get(session) - val cert = unverifiedHandshake.peerCertificates()[0] as X509Certificate - val hostPin = CertificatePinner.pin(cert) - - if (mapping.containsKey(hostname) && mapping[hostname] == hostPin) - return true - else - return defaultVerifier.verify(hostname, session) - } - - } - return pinHostVerifier - } - - internal fun buildHttpClient(c: Context, user: String, password: String, hostname: String): OkHttpClient { - - // TODO: HACK - val THREAD_ID = 10000; - TrafficStats.setThreadStatsTag(THREAD_ID); - - val prefs = c.getSharedPreferences("pinnedCerts", Context.MODE_PRIVATE) - val pinnedHosts: Set = prefs.getStringSet("pinnedHosts", emptySet())!! - - val okHttpClient = OkHttpClient.Builder() - .addInterceptor(BasicAuthInterceptor(user, password)) - .connectTimeout(15, TimeUnit.SECONDS) - - /* Rely on system certificates if we do not have the host pinned */ - if (pinnedHosts.contains(hostname)) { - val cpb = CertificatePinner.Builder() - - pinnedHosts.forEach { ph -> - cpb.add(ph, prefs.getString("pin-${ph}", "")) - } - - - val certPinner = cpb.build() - getCompositeSSLSocketFactory(certPinner, hostname).let { - okHttpClient.sslSocketFactory(it) - } - //okHttpClient.certificatePinner(certPinner) - } - - okHttpClient.hostnameVerifier(getHostNameVerifier(prefs)) - - val client = okHttpClient.build() - return client - - } - - /** - * @param fp Fingerprint in sha 256 format - */ - internal fun addPinnedCert(c: Context, host: String, fp: String) { - val prefs = c.getSharedPreferences("pinnedCerts", Context.MODE_PRIVATE) - val pedit = prefs.edit() - val pinnedHosts: MutableSet = prefs.getStringSet("pinnedHosts", mutableSetOf())!! - - pinnedHosts.add(host) - - pedit.putString("pin-${host}", "sha256/${fp}") - - pedit.putStringSet("pinnedHosts", pinnedHosts) - - pedit.apply() - } - - internal fun removedPinnedCert(c: Context, host: String) { - val prefs = c.getSharedPreferences("pinnedCerts", Context.MODE_PRIVATE) - val pedit = prefs.edit() - val pinnedHosts: MutableSet = prefs.getStringSet("pinnedHosts", mutableSetOf())!! - - pinnedHosts.remove(host) - - pedit.remove("pin-${host}") - - pedit.putStringSet("pinnedHosts", pinnedHosts) - - pedit.apply() - } - - fun fetchProfile(c: Context, asUri: HttpUrl, user: String, password: String): Response? { - - - val httpClient = buildHttpClient(c, user, password, asUri.host() ?: "") - - val request = Request.Builder() - .url(asUri) - .build() - - val response = httpClient.newCall(request).execute() - - return response - - } - - private fun getAsUrl(url: String, autologin: Boolean): HttpUrl { - var asurl = url - if (!asurl.startsWith("http")) - asurl = "https://" + asurl - - if (autologin) - asurl += "/rest/GetAutologin?tls-cryptv2=1" - else - asurl += "/rest/GetUserlogin?tls-cryptv2=1" - - val asUri = HttpUrl.parse(asurl) - return asUri - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return dialogView - } - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val inflater = requireActivity().layoutInflater - dialogView = inflater.inflate(R.layout.import_as_config, null); - - val builder = AlertDialog.Builder(requireContext()) - - builder.setView(dialogView) - builder.setTitle(R.string.import_from_as) - - asServername = dialogView.findViewById(R.id.as_servername) - asUsername = dialogView.findViewById(R.id.username) - asPassword = dialogView.findViewById(R.id.password) - asUseAutlogin = dialogView.findViewById(R.id.request_autologin) - - builder.setPositiveButton(R.string.import_config, null) - builder.setNegativeButton(android.R.string.cancel) { _, _ -> } - - val dialog = builder.create() - - return dialog - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - dialog!!.setOnShowListener() { d2 -> - val d: AlertDialog = d2 as AlertDialog - - d.getButton(AlertDialog.BUTTON_POSITIVE)?.setOnClickListener() - - { _ -> - viewLifecycleOwner.lifecycleScope.launch { - doAsImport(asUsername.text.toString(), asPassword.text.toString()) - } - } - } - } - - val crvMessage = Pattern.compile(".*CRV1:R,E:(.*):(.*):(.*).*", Pattern.DOTALL) - - suspend internal fun doAsImport(user: String, password: String) { - var pleaseWait:AlertDialog? - withContext(Dispatchers.IO) - { - - withContext(Dispatchers.Main) - { - val ab = AlertDialog.Builder(requireContext()) - ab.setTitle("Downloading profile") - ab.setMessage("Please wait") - pleaseWait = ab.show() - - Toast.makeText(context, "Downloading profile", Toast.LENGTH_LONG).show() - } - - - val asProfileUri = getAsUrl(asServername.text.toString(), asUseAutlogin.isChecked) - - var e: Exception? = null - try { - val response = fetchProfile(requireContext(), asProfileUri, user, password) - - if (response == null) { - throw Exception("No Response from Server") - } - - val profile = response.body().string() - if (response.code() == 401 && crvMessage.matcher(profile).matches()) { - withContext(Dispatchers.Main) { - pleaseWait?.dismiss() - showCRDialog(profile) - } - } else if (response.isSuccessful) { - - withContext(Dispatchers.Main) { - pleaseWait?.dismiss() - val startImport = Intent(activity, ConfigConverter::class.java) - startImport.action = ConfigConverter.IMPORT_PROFILE_DATA - startImport.putExtra(Intent.EXTRA_TEXT, profile) - startActivity(startImport) - dismiss() - } - } else { - throw Exception("Invalid Response from server: \n${response.code()} ${response.message()} \n\n ${profile}") - } - - } catch (ce: SSLHandshakeException) { - e = ce - // Find out if we are in the non trust path - if (ce.cause is CertificateException && ce.cause != null) { - val certExp: CertificateException = (ce.cause as CertificateException) - if (certExp.cause is CertPathValidatorException && certExp.cause != null) { - val caPathExp: CertPathValidatorException = certExp.cause as CertPathValidatorException - if (caPathExp.certPath.type.equals("X.509") && caPathExp.certPath.certificates.size > 0) { - val firstCert: X509Certificate = (caPathExp.certPath.certificates[0] as X509Certificate) - - val fpBytes = MessageDigest.getInstance("SHA-256").digest(firstCert.publicKey.encoded) - val fp = Base64.encodeToString(fpBytes, NO_WRAP) - - - - Log.i("OpenVPN", "Found cert with FP ${fp}: ${firstCert.subjectDN}") - withContext(Dispatchers.Main) { - - pleaseWait?.dismiss() - - AlertDialog.Builder(requireContext()) - .setTitle("Untrusted certificate found") - .setMessage(firstCert.toString()) - .setPositiveButton("Trust") { _, _ -> addPinnedCert(requireContext(), asProfileUri.host(), fp) } - .setNegativeButton("Do not trust", null) - .show() - } - e = null - } - } else if (ce.message != null && ce.message!!.contains("Certificate pinning failure")) { - withContext(Dispatchers.Main) { - pleaseWait?.dismiss() - - AlertDialog.Builder(requireContext()) - .setTitle("Different certificate than trusted certificate from server") - .setMessage(ce.message) - .setNegativeButton(android.R.string.ok, null) - .setPositiveButton("Forget pinned certificate", { _, _ -> removedPinnedCert(requireContext(), asProfileUri.host()) }) - .show(); - } - e = null - - } - } - } catch (ge: Exception) { - e = ge - } - if (e != null) { - withContext(Dispatchers.Main) { - pleaseWait?.dismiss() - AlertDialog.Builder(requireContext()) - .setTitle("Import failed") - .setMessage("Error: " + e.localizedMessage) - .setPositiveButton(android.R.string.ok, null) - .show() - } - } - } - } - - private fun showCRDialog(response: String) { - // This is a dirty hack instead of properly parsing the response - val m = crvMessage.matcher(response) - // We already know that it matches - m.matches() - val challenge = m.group(1) - var username = m.group(2) - val message = m.group(3) - - username = String(Base64.decode(username, Base64.DEFAULT)) - - val pwprefix = "CRV1::${challenge}::" - - val entry = EditText(context) - entry.setInputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD) - - AlertDialog.Builder(requireContext()) - .setTitle("Server request challenge/response authentication") - .setMessage("Challenge: " + message) - .setView(entry) - .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton(R.string.import_config) { _,_ -> - viewLifecycleOwner.lifecycleScope.launch { - doAsImport(username, pwprefix + entry.text.toString()) - } - } - .show() - - } - - - override fun onResume() { - super.onResume() - asServername.setText(Preferences.getDefaultSharedPreferences(activity).getString("as-hostname", "")) - asUsername.setText(Preferences.getDefaultSharedPreferences(activity).getString("as-username", "")) - } - - override fun onPause() { - super.onPause() - val prefs = Preferences.getDefaultSharedPreferences(activity) - prefs.edit().putString("as-hostname", asServername.text.toString()).apply() - prefs.edit().putString("as-username", asUsername.text.toString()).apply() - } - - companion object { - @JvmStatic - fun newInstance(): ImportASConfig { - return ImportASConfig(); - } - } - -} diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/ImportRemoteConfig.kt b/main/src/ui/java/de/blinkt/openvpn/fragments/ImportRemoteConfig.kt new file mode 100644 index 00000000..6cd322ca --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/fragments/ImportRemoteConfig.kt @@ -0,0 +1,465 @@ +/* + * Copyright (c) 2012-2019 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.fragments + +import android.app.AlertDialog +import android.app.Dialog +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.net.TrafficStats +import android.os.Bundle +import android.text.InputType +import android.util.Base64 +import android.util.Base64.NO_WRAP +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.* +import androidx.fragment.app.DialogFragment +import androidx.lifecycle.lifecycleScope +import de.blinkt.openvpn.R +import de.blinkt.openvpn.activities.ConfigConverter +import de.blinkt.openvpn.core.Preferences +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import okhttp3.* +import okhttp3.internal.tls.OkHostnameVerifier +import java.io.IOException +import java.security.MessageDigest +import java.security.cert.CertPathValidatorException +import java.security.cert.CertificateException +import java.security.cert.X509Certificate +import java.util.concurrent.TimeUnit +import java.util.regex.Pattern +import javax.net.ssl.* + +class BasicAuthInterceptor(user: String, password: String) : Interceptor { + + private val credentials: String + + init { + this.credentials = Credentials.basic(user, password) + } + + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val authenticatedRequest = request.newBuilder() + .header("Authorization", credentials).build() + return chain.proceed(authenticatedRequest) + } + +} + + +fun getCompositeSSLSocketFactory(certPin: CertificatePinner, hostname: String): SSLSocketFactory { + val trustPinnedCerts = arrayOf(object : X509TrustManager { + override fun getAcceptedIssuers(): Array { + return emptyArray() + } + + @Throws(CertificateException::class) + override fun checkClientTrusted(chain: Array, authType: String) { + throw CertificateException("Why would we check client certificates?!") + } + + @Throws(CertificateException::class) + override fun checkServerTrusted(chain: Array, authType: String) { + certPin.check(hostname, chain.toList()) + } + }) + + // Install the all-trusting trust manager + val sslContext = SSLContext.getInstance("TLS") + sslContext.init(null, trustPinnedCerts, java.security.SecureRandom()) + // Create an ssl socket factory with our all-trusting manager + + return sslContext.socketFactory + +} + +class ImportRemoteConfig : DialogFragment() { + private lateinit var asUseAutologin: CheckBox + private lateinit var asServername: EditText + private lateinit var asUsername: EditText + private lateinit var asPassword: EditText + private lateinit var dialogView: View + + private lateinit var importChoiceGroup: RadioGroup + private lateinit var importChoiceAS: RadioButton + + + + internal fun getHostNameVerifier(prefs: SharedPreferences): HostnameVerifier { + val pinnedHostnames: Set = prefs.getStringSet("pinnedHosts", emptySet())!! + + val mapping = mutableMapOf() + + pinnedHostnames.forEach { ph -> + mapping[ph] = prefs.getString("pin-${ph}", "") + } + + val defaultVerifier = OkHostnameVerifier.INSTANCE; + val pinHostVerifier = object : HostnameVerifier { + override fun verify(hostname: String?, session: SSLSession?): Boolean { + val unverifiedHandshake = Handshake.get(session) + val cert = unverifiedHandshake.peerCertificates()[0] as X509Certificate + val hostPin = CertificatePinner.pin(cert) + + if (mapping.containsKey(hostname) && mapping[hostname] == hostPin) + return true + else + return defaultVerifier.verify(hostname, session) + } + + } + return pinHostVerifier + } + + internal fun buildHttpClient(c: Context, user: String, password: String, hostname: String): OkHttpClient { + + // TODO: HACK + val THREAD_ID = 10000; + TrafficStats.setThreadStatsTag(THREAD_ID); + + val prefs = c.getSharedPreferences("pinnedCerts", Context.MODE_PRIVATE) + val pinnedHosts: Set = prefs.getStringSet("pinnedHosts", emptySet())!! + + val okHttpClient = OkHttpClient.Builder() + if (user.isNotBlank() && password.isNotBlank()) { + okHttpClient.addInterceptor(BasicAuthInterceptor(user, password)) + } + okHttpClient.connectTimeout(15, TimeUnit.SECONDS) + + /* Rely on system certificates if we do not have the host pinned */ + if (pinnedHosts.contains(hostname)) { + val cpb = CertificatePinner.Builder() + + pinnedHosts.forEach { ph -> + cpb.add(ph, prefs.getString("pin-${ph}", "")) + } + + + val certPinner = cpb.build() + getCompositeSSLSocketFactory(certPinner, hostname).let { + okHttpClient.sslSocketFactory(it) + } + //okHttpClient.certificatePinner(certPinner) + } + + okHttpClient.hostnameVerifier(getHostNameVerifier(prefs)) + + val client = okHttpClient.build() + return client + + } + + /** + * @param fp Fingerprint in sha 256 format + */ + internal fun addPinnedCert(c: Context, host: String, fp: String) { + val prefs = c.getSharedPreferences("pinnedCerts", Context.MODE_PRIVATE) + val pedit = prefs.edit() + val pinnedHosts: MutableSet = prefs.getStringSet("pinnedHosts", mutableSetOf())!! + .toMutableSet() + + pinnedHosts.add(host) + + pedit.putString("pin-${host}", "sha256/${fp}") + + pedit.putStringSet("pinnedHosts", pinnedHosts) + + pedit.apply() + } + + internal fun removedPinnedCert(c: Context, host: String) { + val prefs = c.getSharedPreferences("pinnedCerts", Context.MODE_PRIVATE) + val pedit = prefs.edit() + val pinnedHosts: MutableSet = prefs.getStringSet("pinnedHosts", mutableSetOf())!!.toMutableSet() + + pinnedHosts.remove(host) + + pedit.remove("pin-${host}") + + pedit.putStringSet("pinnedHosts", pinnedHosts) + + pedit.apply() + } + + fun fetchProfile(c: Context, asUri: HttpUrl, user: String, password: String): Response? { + + + val httpClient = buildHttpClient(c, user, password, asUri.host() ?: "") + + val request = Request.Builder() + .url(asUri) + .build() + + val response = httpClient.newCall(request).execute() + + return response + + } + + private fun getAsUrl(url: String, autologin: Boolean): HttpUrl { + var asurl = url + if (!asurl.startsWith("http")) + asurl = "https://" + asurl + + if (autologin) + asurl += "/rest/GetAutologin?tls-cryptv2=1" + else + asurl += "/rest/GetUserlogin?tls-cryptv2=1" + + val asUri = HttpUrl.parse(asurl) + return asUri + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return dialogView + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val inflater = requireActivity().layoutInflater + dialogView = inflater.inflate(R.layout.import_remote_config, null); + + val builder = AlertDialog.Builder(requireContext()) + + builder.setView(dialogView) + builder.setTitle(R.string.import_from_as) + + asServername = dialogView.findViewById(R.id.as_servername) + asUsername = dialogView.findViewById(R.id.username) + asPassword = dialogView.findViewById(R.id.password) + asUseAutologin = dialogView.findViewById(R.id.request_autologin) + + importChoiceGroup = dialogView.findViewById(R.id.import_source_group) + importChoiceAS = dialogView.findViewById(R.id.import_choice_as) + + importChoiceGroup.setOnCheckedChangeListener { group, checkedId -> + if (checkedId == R.id.import_choice_as) + asServername.setHint(R.string.as_servername) + else + asServername.setHint(R.string.server_url) + } + + builder.setPositiveButton(R.string.import_config, null) + builder.setNegativeButton(android.R.string.cancel) { _, _ -> } + + if (arguments?.getString("url") != null) + { + asServername.setText(arguments?.getString("url")) + importChoiceGroup.check(R.id.import_choice_url) + } + + val dialog = builder.create() + + return dialog + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + dialog!!.setOnShowListener() { d2 -> + val d: AlertDialog = d2 as AlertDialog + + d.getButton(AlertDialog.BUTTON_POSITIVE)?.setOnClickListener() + + { _ -> + viewLifecycleOwner.lifecycleScope.launch { + doAsImport(asUsername.text.toString(), asPassword.text.toString()) + } + } + } + } + + val crvMessage = Pattern.compile(".*CRV1:R,E:(.*):(.*):(.*).*", Pattern.DOTALL) + + suspend internal fun doAsImport(user: String, password: String) { + var pleaseWait:AlertDialog? + withContext(Dispatchers.IO) + { + + withContext(Dispatchers.Main) + { + val ab = AlertDialog.Builder(requireContext()) + ab.setTitle("Downloading profile") + ab.setMessage("Please wait") + pleaseWait = ab.show() + + Toast.makeText(context, "Downloading profile", Toast.LENGTH_LONG).show() + } + + val asProfileUri:HttpUrl + if (importChoiceAS.isChecked) + asProfileUri = getAsUrl(asServername.text.toString(), asUseAutologin.isChecked) + else + asProfileUri = HttpUrl.parse(asServername.text.toString()) + + var e: Exception? = null + try { + val response = fetchProfile(requireContext(), asProfileUri, user, password) + + if (response == null) { + throw Exception("No Response from Server") + } + + val profile = response.body().string() + if (response.code() == 401 && crvMessage.matcher(profile).matches()) { + withContext(Dispatchers.Main) { + pleaseWait?.dismiss() + showCRDialog(profile) + } + } else if (response.isSuccessful) { + + withContext(Dispatchers.Main) { + pleaseWait?.dismiss() + val startImport = Intent(activity, ConfigConverter::class.java) + startImport.action = ConfigConverter.IMPORT_PROFILE_DATA + startImport.putExtra(Intent.EXTRA_TEXT, profile) + startActivity(startImport) + dismiss() + } + } else { + throw Exception("Invalid Response from server: \n${response.code()} ${response.message()} \n\n ${profile}") + } + + } catch (ce: SSLHandshakeException) { + e = ce + // Find out if we are in the non trust path + if (ce.cause is CertificateException && ce.cause != null) { + val certExp: CertificateException = (ce.cause as CertificateException) + if (certExp.cause is CertPathValidatorException && certExp.cause != null) { + val caPathExp: CertPathValidatorException = certExp.cause as CertPathValidatorException + if (caPathExp.certPath.type.equals("X.509") && caPathExp.certPath.certificates.size > 0) { + val firstCert: X509Certificate = (caPathExp.certPath.certificates[0] as X509Certificate) + + val fpBytes = MessageDigest.getInstance("SHA-256").digest(firstCert.publicKey.encoded) + val fp = Base64.encodeToString(fpBytes, NO_WRAP) + + + + Log.i("OpenVPN", "Found cert with FP ${fp}: ${firstCert.subjectDN}") + withContext(Dispatchers.Main) { + + pleaseWait?.dismiss() + + AlertDialog.Builder(requireContext()) + .setTitle("Untrusted certificate found") + .setMessage(firstCert.toString()) + .setPositiveButton("Trust") { _, _ -> addPinnedCert(requireContext(), asProfileUri.host(), fp) } + .setNegativeButton("Do not trust", null) + .show() + } + e = null + } + } else if (ce.message != null && ce.message!!.contains("Certificate pinning failure")) { + withContext(Dispatchers.Main) { + pleaseWait?.dismiss() + + AlertDialog.Builder(requireContext()) + .setTitle("Different certificate than trusted certificate from server") + .setMessage(ce.message) + .setNegativeButton(android.R.string.ok, null) + .setPositiveButton("Forget pinned certificate", { _, _ -> removedPinnedCert(requireContext(), asProfileUri.host()) }) + .show(); + } + e = null + + } + } + } catch (ge: Exception) { + e = ge + } + if (e != null) { + withContext(Dispatchers.Main) { + pleaseWait?.dismiss() + AlertDialog.Builder(requireContext()) + .setTitle("Import failed") + .setMessage("Error: " + e.localizedMessage) + .setPositiveButton(android.R.string.ok, null) + .show() + } + } + } + } + + private fun showCRDialog(response: String) { + // This is a dirty hack instead of properly parsing the response + val m = crvMessage.matcher(response) + // We already know that it matches + m.matches() + val challenge = m.group(1) + var username = m.group(2) + val message = m.group(3) + + username = String(Base64.decode(username, Base64.DEFAULT)) + + val pwprefix = "CRV1::${challenge}::" + + val entry = EditText(context) + entry.setInputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD) + + AlertDialog.Builder(requireContext()) + .setTitle("Server request challenge/response authentication") + .setMessage("Challenge: " + message) + .setView(entry) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(R.string.import_config) { _,_ -> + viewLifecycleOwner.lifecycleScope.launch { + doAsImport(username, pwprefix + entry.text.toString()) + } + } + .show() + + } + + + override fun onResume() { + super.onResume() + if (arguments == null) { + asServername.setText( + Preferences.getDefaultSharedPreferences(activity).getString("as-hostname", "") + ) + asUsername.setText( + Preferences.getDefaultSharedPreferences(activity).getString("as-username", "") + ) + if (Preferences.getDefaultSharedPreferences(activity).getBoolean("as-selected", true)) { + importChoiceGroup.check(R.id.import_choice_as) + } else { + importChoiceGroup.check(R.id.import_choice_url) + } + + } + } + + override fun onPause() { + super.onPause() + val prefs = Preferences.getDefaultSharedPreferences(activity) + val editor = prefs.edit() + editor.putString("as-hostname", asServername.text.toString()) + editor.putString("as-username", asUsername.text.toString()) + editor.putBoolean("as-selected", importChoiceAS.isChecked) + editor.apply() + } + + companion object { + @JvmStatic + fun newInstance(url:String? = null): ImportRemoteConfig { + val frag = ImportRemoteConfig() + if (url != null) + { + val extras = Bundle() + extras.putString("url", url) + frag.arguments = extras + } + return frag + } + } +} diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.java b/main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.java index 3804926f..ce6fa7f1 100644 --- a/main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.java +++ b/main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.java @@ -21,7 +21,6 @@ import android.os.Bundle; import android.os.PersistableBundle; import androidx.annotation.RequiresApi; -import androidx.fragment.app.DialogFragment; import androidx.fragment.app.ListFragment; import android.text.Html; @@ -95,7 +94,7 @@ public class VPNProfileList extends ListFragment implements OnClickListener, Vpn private boolean showUserRequestDialogIfNeeded(ConnectionStatus level, Intent intent) { if (level == LEVEL_WAITING_FOR_USER_INPUT) { - if (intent.getStringExtra(EXTRA_CHALLENGE_TXT) != null) { + if (intent != null && intent.getStringExtra(EXTRA_CHALLENGE_TXT) != null) { PasswordDialogFragment pwInputFrag = PasswordDialogFragment.Companion.newInstance(intent, false); pwInputFrag.show(getParentFragmentManager(), "dialog"); @@ -126,6 +125,7 @@ public class VPNProfileList extends ListFragment implements OnClickListener, Vpn public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); + setListAdapter(); } @RequiresApi(api = Build.VERSION_CODES.N_MR1) @@ -277,12 +277,6 @@ public class VPNProfileList extends ListFragment implements OnClickListener, Vpn } - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - setListAdapter(); - } - private void setListAdapter() { if (mArrayadapter == null) { mArrayadapter = new VPNArrayAdapter(getActivity(), R.layout.vpn_list_item, R.id.vpn_item_title); @@ -354,8 +348,8 @@ public class VPNProfileList extends ListFragment implements OnClickListener, Vpn } private boolean startASProfileImport() { - ImportASConfig asImportFrag = ImportASConfig.newInstance(); - asImportFrag.show(requireFragmentManager(), "dialog"); + ImportRemoteConfig asImportFrag = ImportRemoteConfig.newInstance(null); + asImportFrag.show(getParentFragmentManager(), "dialog"); return true; } -- cgit v1.2.3