From 3689df1385b31343ff537334bd3d71cb7f074959 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Sat, 13 Jan 2018 22:19:53 +0100 Subject: [PATCH] Login with client certificates * setup UI: login with URL and client certificate * account settings UI: show either username/password or client certificate alias * AccountSettings: serve credentials in generalized Credentials objects * HttpClient: use Credentials (instead of username/password) for authentication * HttpClient: always use CustomTlsSocketFactory * CustomTlsSocketFactory: support client certificates --- .../davdroid/SSLSocketFactoryCompatTest.java | 7 +- .../ui/setup/DavResourceFinderTest.java | 9 +- .../at/bitfire/davdroid/AccountSettings.kt | 27 ++- ...oryCompat.kt => CustomTlsSocketFactory.kt} | 18 +- .../java/at/bitfire/davdroid/HttpClient.kt | 90 +++++++-- .../at/bitfire/davdroid/model/Credentials.kt | 41 ++++ .../syncadapter/ContactsSyncManager.kt | 7 +- .../davdroid/ui/AccountSettingsActivity.kt | 55 +++-- .../ui/setup/AccountDetailsFragment.kt | 11 +- .../davdroid/ui/setup/DavResourceFinder.kt | 12 +- .../setup/DefaultLoginCredentialsFragment.kt | 188 +++++++++++------- .../ui/setup/DetectConfigurationFragment.kt | 4 +- .../{LoginCredentials.kt => LoginInfo.kt} | 23 ++- .../res/layout/login_credentials_fragment.xml | 44 +++- app/src/main/res/values/strings.xml | 6 + app/src/main/res/xml/settings_account.xml | 5 + dav4android | 2 +- vcard4android | 2 +- 18 files changed, 390 insertions(+), 161 deletions(-) rename app/src/main/java/at/bitfire/davdroid/{SSLSocketFactoryCompat.kt => CustomTlsSocketFactory.kt} (94%) create mode 100644 app/src/main/java/at/bitfire/davdroid/model/Credentials.kt rename app/src/main/java/at/bitfire/davdroid/ui/setup/{LoginCredentials.kt => LoginInfo.kt} (60%) diff --git a/app/src/androidTest/java/at/bitfire/davdroid/SSLSocketFactoryCompatTest.java b/app/src/androidTest/java/at/bitfire/davdroid/SSLSocketFactoryCompatTest.java index da8ad7085..ac996a73d 100644 --- a/app/src/androidTest/java/at/bitfire/davdroid/SSLSocketFactoryCompatTest.java +++ b/app/src/androidTest/java/at/bitfire/davdroid/SSLSocketFactoryCompatTest.java @@ -16,14 +16,11 @@ import java.net.Socket; import javax.net.ssl.SSLSocket; -import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509TrustManager; import at.bitfire.cert4android.CustomCertManager; import okhttp3.mockwebserver.MockWebServer; import static android.support.test.InstrumentationRegistry.getInstrumentation; -import static android.support.test.InstrumentationRegistry.getTargetContext; import static junit.framework.TestCase.assertFalse; import static org.apache.commons.lang3.ArrayUtils.contains; import static org.junit.Assert.assertTrue; @@ -31,13 +28,13 @@ public class SSLSocketFactoryCompatTest { CustomCertManager certMgr; - SSLSocketFactoryCompat factory; + CustomTlsSocketFactory factory; MockWebServer server = new MockWebServer(); @Before public void startServer() throws Exception { certMgr = new CustomCertManager(getInstrumentation().getContext(), false, true); - factory = new SSLSocketFactoryCompat(certMgr); + factory = new CustomTlsSocketFactory(null, certMgr); server.start(); } diff --git a/app/src/androidTest/java/at/bitfire/davdroid/ui/setup/DavResourceFinderTest.java b/app/src/androidTest/java/at/bitfire/davdroid/ui/setup/DavResourceFinderTest.java index 77cb31439..019a70751 100644 --- a/app/src/androidTest/java/at/bitfire/davdroid/ui/setup/DavResourceFinderTest.java +++ b/app/src/androidTest/java/at/bitfire/davdroid/ui/setup/DavResourceFinderTest.java @@ -22,6 +22,7 @@ import at.bitfire.dav4android.property.ResourceType; import at.bitfire.davdroid.HttpClient; import at.bitfire.davdroid.log.Logger; +import at.bitfire.davdroid.model.Credentials; import at.bitfire.davdroid.ui.setup.DavResourceFinder.Configuration.ServiceInfo; import okhttp3.mockwebserver.Dispatcher; import okhttp3.mockwebserver.MockResponse; @@ -40,7 +41,7 @@ public class DavResourceFinderTest { DavResourceFinder finder; HttpClient client; - LoginCredentials credentials; + LoginInfo loginInfo; private static final String PATH_NO_DAV = "/nodav", @@ -58,11 +59,11 @@ public void initServerAndClient() throws Exception { server.setDispatcher(new TestDispatcher()); server.start(); - credentials = new LoginCredentials(URI.create("/"), "mock", "12345"); - finder = new DavResourceFinder(getTargetContext(), credentials); + loginInfo = new LoginInfo(URI.create("/"), new Credentials("mock", "12345")); + finder = new DavResourceFinder(getTargetContext(), loginInfo); client = new HttpClient.Builder() - .addAuthentication(null, credentials.getUserName(), credentials.getPassword()) + .addAuthentication(null, loginInfo.credentials) .build(); } diff --git a/app/src/main/java/at/bitfire/davdroid/AccountSettings.kt b/app/src/main/java/at/bitfire/davdroid/AccountSettings.kt index d3c826e5e..8cb74341f 100644 --- a/app/src/main/java/at/bitfire/davdroid/AccountSettings.kt +++ b/app/src/main/java/at/bitfire/davdroid/AccountSettings.kt @@ -18,6 +18,7 @@ import android.provider.CalendarContract import android.provider.ContactsContract import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.model.CollectionInfo +import at.bitfire.davdroid.model.Credentials import at.bitfire.davdroid.model.ServiceDB import at.bitfire.davdroid.model.ServiceDB.* import at.bitfire.davdroid.model.ServiceDB.Collections @@ -49,6 +50,7 @@ class AccountSettings @Throws(InvalidAccountException::class) constructor( val KEY_SETTINGS_VERSION = "version" val KEY_USERNAME = "user_name" + val KEY_CERTIFICATE_ALIAS = "certificate_alias" val KEY_WIFI_ONLY = "wifi_only" // sync on WiFi only (default: false) val KEY_WIFI_ONLY_SSIDS = "wifi_only_ssids" // restrict sync to specific WiFi SSIDs @@ -80,10 +82,17 @@ class AccountSettings @Throws(InvalidAccountException::class) constructor( @JvmField val SYNC_INTERVAL_MANUALLY = -1L - fun initialUserData(userName: String): Bundle { + fun initialUserData(credentials: Credentials): Bundle { val bundle = Bundle(2) bundle.putString(KEY_SETTINGS_VERSION, CURRENT_VERSION.toString()) - bundle.putString(KEY_USERNAME, userName) + + when (credentials.type) { + Credentials.Type.UsernamePassword -> + bundle.putString(KEY_USERNAME, credentials.userName) + Credentials.Type.ClientCertificate -> + bundle.putString(KEY_CERTIFICATE_ALIAS, credentials.certificateAlias) + } + return bundle } @@ -110,11 +119,17 @@ class AccountSettings @Throws(InvalidAccountException::class) constructor( // authentication settings - fun username(): String? = accountManager.getUserData(account, KEY_USERNAME) - fun username(userName: String) = accountManager.setUserData(account, KEY_USERNAME, userName) + fun credentials() = Credentials( + accountManager.getUserData(account, KEY_USERNAME), + accountManager.getPassword(account), + accountManager.getUserData(account, KEY_CERTIFICATE_ALIAS) + ) - fun password(): String? = accountManager.getPassword(account) - fun password(password: String) = accountManager.setPassword(account, password) + fun credentials(credentials: Credentials) { + accountManager.setUserData(account, KEY_USERNAME, credentials.userName) + accountManager.setPassword(account, credentials.password) + accountManager.setUserData(account, KEY_CERTIFICATE_ALIAS, credentials.certificateAlias) + } // sync. settings diff --git a/app/src/main/java/at/bitfire/davdroid/SSLSocketFactoryCompat.kt b/app/src/main/java/at/bitfire/davdroid/CustomTlsSocketFactory.kt similarity index 94% rename from app/src/main/java/at/bitfire/davdroid/SSLSocketFactoryCompat.kt rename to app/src/main/java/at/bitfire/davdroid/CustomTlsSocketFactory.kt index cbc61f1a6..9ade6eed1 100644 --- a/app/src/main/java/at/bitfire/davdroid/SSLSocketFactoryCompat.kt +++ b/app/src/main/java/at/bitfire/davdroid/CustomTlsSocketFactory.kt @@ -15,12 +15,15 @@ import java.net.InetAddress import java.net.Socket import java.security.GeneralSecurityException import java.util.* -import javax.net.ssl.SSLContext -import javax.net.ssl.SSLSocket -import javax.net.ssl.SSLSocketFactory -import javax.net.ssl.X509TrustManager +import javax.net.ssl.* -class SSLSocketFactoryCompat( +/** + * Custom TLS socket factory with support for + * - enabling/disabling algorithms depending on the Android version, + * - client certificate authentication + */ +class CustomTlsSocketFactory( + keyManager: KeyManager?, trustManager: X509TrustManager ): SSLSocketFactory() { @@ -105,7 +108,10 @@ class SSLSocketFactoryCompat( init { try { val sslContext = SSLContext.getInstance("TLS") - sslContext.init(null, arrayOf(trustManager), null) + sslContext.init( + if (keyManager != null) arrayOf(keyManager) else null, + arrayOf(trustManager), + null) delegate = sslContext.socketFactory } catch (e: GeneralSecurityException) { throw IllegalStateException() // system has no TLS diff --git a/app/src/main/java/at/bitfire/davdroid/HttpClient.kt b/app/src/main/java/at/bitfire/davdroid/HttpClient.kt index 7597d7cad..dd6442088 100644 --- a/app/src/main/java/at/bitfire/davdroid/HttpClient.kt +++ b/app/src/main/java/at/bitfire/davdroid/HttpClient.kt @@ -10,10 +10,12 @@ package at.bitfire.davdroid import android.content.Context import android.os.Build +import android.security.KeyChain import at.bitfire.cert4android.CustomCertManager import at.bitfire.dav4android.BasicDigestAuthHandler import at.bitfire.dav4android.UrlUtils import at.bitfire.davdroid.log.Logger +import at.bitfire.davdroid.model.Credentials import at.bitfire.davdroid.settings.ISettings import okhttp3.Cache import okhttp3.Interceptor @@ -25,10 +27,17 @@ import java.io.Closeable import java.io.File import java.net.InetSocketAddress import java.net.Proxy +import java.net.Socket +import java.security.KeyStore +import java.security.Principal import java.text.DateFormat import java.util.* import java.util.concurrent.TimeUnit import java.util.logging.Level +import javax.net.ssl.KeyManager +import javax.net.ssl.TrustManagerFactory +import javax.net.ssl.X509ExtendedKeyManager +import javax.net.ssl.X509TrustManager class HttpClient private constructor( val okHttpClient: OkHttpClient, @@ -60,9 +69,11 @@ class HttpClient private constructor( val context: Context? = null, val settings: ISettings? = null, accountSettings: AccountSettings? = null, - logger: java.util.logging.Logger = Logger.log + val logger: java.util.logging.Logger = Logger.log ) { private var certManager: CustomCertManager? = null + private var certificateAlias: String? = null + private val orig = sharedClient.newBuilder() init { @@ -101,17 +112,14 @@ class HttpClient private constructor( // use account settings for authentication accountSettings?.let { - val userName = accountSettings.username() - val password = accountSettings.password() - if (userName != null && password != null) - addAuthentication(null, userName, password) + addAuthentication(null, it.credentials()) } } } } - constructor(context: Context, host: String?, username: String, password: String): this(context) { - addAuthentication(host, username, password) + constructor(context: Context, host: String?, credentials: Credentials): this(context) { + addAuthentication(host, credentials) } fun withDiskCache(): Builder { @@ -135,22 +143,76 @@ class HttpClient private constructor( fun customCertManager(manager: CustomCertManager) { certManager = manager - orig.sslSocketFactory(SSLSocketFactoryCompat(manager), manager) - orig.hostnameVerifier(manager.hostnameVerifier(OkHostnameVerifier.INSTANCE)) } fun setForeground(foreground: Boolean): Builder { certManager?.appInForeground = foreground return this } - fun addAuthentication(host: String?, username: String, password: String): Builder { - val authHandler = BasicDigestAuthHandler(UrlUtils.hostToDomain(host), username, password) - orig .addNetworkInterceptor(authHandler) - .authenticator(authHandler) + fun addAuthentication(host: String?, credentials: Credentials): Builder { + when (credentials.type) { + Credentials.Type.UsernamePassword -> { + val authHandler = BasicDigestAuthHandler(UrlUtils.hostToDomain(host), credentials.userName!!, credentials.password!!) + orig .addNetworkInterceptor(authHandler) + .authenticator(authHandler) + } + Credentials.Type.ClientCertificate -> { + certificateAlias = credentials.certificateAlias + } + } return this } - fun build() = HttpClient(orig.build(), certManager) + fun build(): HttpClient { + val trustManager = certManager ?: { + val factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) + factory.init(null as KeyStore?) + factory.trustManagers.first() as X509TrustManager + }() + + val hostnameVerifier = certManager?.hostnameVerifier(OkHostnameVerifier.INSTANCE) + ?: OkHostnameVerifier.INSTANCE + + var keyManager: KeyManager? = null + try { + certificateAlias?.let { alias -> + // get client certificate and private key + val certs = KeyChain.getCertificateChain(context, alias) ?: return@let + val key = KeyChain.getPrivateKey(context, alias) ?: return@let + logger.fine("Using client certificate $alias for authentication (chain length: ${certs.size})") + + // create Android KeyStore (performs key operations without revealing secret data to DAVdroid) + val keyStore = KeyStore.getInstance("AndroidKeyStore") + keyStore.load(null) + + // create KeyManager + keyManager = object: X509ExtendedKeyManager() { + override fun getServerAliases(p0: String?, p1: Array?): Array? = null + override fun chooseServerAlias(p0: String?, p1: Array?, p2: Socket?) = null + + override fun getClientAliases(p0: String?, p1: Array?) = + arrayOf(alias) + + override fun chooseClientAlias(p0: Array?, p1: Array?, p2: Socket?) = + alias + + override fun getCertificateChain(forAlias: String?) = + certs.takeIf { forAlias == alias } + + override fun getPrivateKey(forAlias: String?) = + key.takeIf { forAlias == alias } + } + } + } catch (e: Exception) { + logger.log(Level.SEVERE, "Couldn't set up client certificate authentication", e) + } + + orig.sslSocketFactory(CustomTlsSocketFactory(keyManager, trustManager), trustManager) + orig.hostnameVerifier(hostnameVerifier) + + return HttpClient(orig.build(), certManager) + } + } diff --git a/app/src/main/java/at/bitfire/davdroid/model/Credentials.kt b/app/src/main/java/at/bitfire/davdroid/model/Credentials.kt new file mode 100644 index 000000000..872e01b91 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/model/Credentials.kt @@ -0,0 +1,41 @@ +/* + * Copyright © Ricki Hirner (bitfire web engineering). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package at.bitfire.davdroid.model + +import java.io.Serializable + +class Credentials @JvmOverloads constructor( + @JvmField val userName: String? = null, + @JvmField val password: String? = null, + @JvmField val certificateAlias: String? = null +): Serializable { + + enum class Type { + UsernamePassword, + ClientCertificate + } + + val type: Type + + init { + type = when { + !certificateAlias.isNullOrEmpty() -> + Type.ClientCertificate + !userName.isNullOrEmpty() && !password.isNullOrEmpty() -> + Type.UsernamePassword + else -> + throw IllegalArgumentException("Either username/password or certificate alias must be set") + } + } + + override fun toString(): String { + return "Credentials(type=$type, userName=$userName, certificateAlias=$certificateAlias)" + } + +} \ No newline at end of file diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncManager.kt b/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncManager.kt index 6cc1a0365..7ddd07a99 100644 --- a/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncManager.kt +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncManager.kt @@ -483,12 +483,7 @@ class ContactsSyncManager( } // authenticate only against a certain host, and only upon request - val username = accountSettings.username() - val password = accountSettings.password() - val builder = if (username != null && password != null) - HttpClient.Builder(context, baseUrl.host(), username, password) - else - HttpClient.Builder(context) + val builder = HttpClient.Builder(context, baseUrl.host(), accountSettings.credentials()) // allow redirects builder.followRedirects(true) diff --git a/app/src/main/java/at/bitfire/davdroid/ui/AccountSettingsActivity.kt b/app/src/main/java/at/bitfire/davdroid/ui/AccountSettingsActivity.kt index 137777a54..3a9568bcb 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/AccountSettingsActivity.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/AccountSettingsActivity.kt @@ -13,7 +13,10 @@ import android.app.DialogFragment import android.app.LoaderManager import android.content.* import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.provider.CalendarContract +import android.security.KeyChain import android.support.v14.preference.PreferenceFragment import android.support.v4.app.NavUtils import android.support.v7.app.AlertDialog @@ -26,6 +29,7 @@ import android.view.MenuItem import at.bitfire.davdroid.AccountSettings import at.bitfire.davdroid.InvalidAccountException import at.bitfire.davdroid.R +import at.bitfire.davdroid.model.Credentials import at.bitfire.davdroid.settings.ISettings import at.bitfire.ical4android.TaskProvider import at.bitfire.vcard4android.GroupMethod @@ -86,19 +90,46 @@ class AccountSettingsActivity: AppCompatActivity() { // preference group: authentication val prefUserName = findPreference("username") as EditTextPreference - prefUserName.summary = accountSettings.username() - prefUserName.text = accountSettings.username() - prefUserName.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> - accountSettings.username(newValue as String) - loaderManager.restartLoader(0, arguments, this) - false - } - val prefPassword = findPreference("password") as EditTextPreference - prefPassword.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> - accountSettings.password(newValue as String) - loaderManager.restartLoader(0, arguments, this) - false + val prefCertAlias = findPreference("certificate_alias") as Preference + + val credentials = accountSettings.credentials() + when (credentials.type) { + Credentials.Type.UsernamePassword -> { + prefUserName.isVisible = true + prefUserName.summary = credentials.userName + prefUserName.text = credentials.userName + prefUserName.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> + accountSettings.credentials(Credentials(newValue as String, credentials.password)) + loaderManager.restartLoader(0, arguments, this) + false + } + + prefPassword.isVisible = true + prefPassword.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> + accountSettings.credentials(Credentials(credentials.userName, newValue as String)) + loaderManager.restartLoader(0, arguments, this) + false + } + + prefCertAlias.isVisible = false + } + Credentials.Type.ClientCertificate -> { + prefUserName.isVisible = false + prefPassword.isVisible = false + + prefCertAlias.isVisible = true + prefCertAlias.summary = credentials.certificateAlias + prefCertAlias.setOnPreferenceClickListener { + KeyChain.choosePrivateKeyAlias(activity, { alias -> + accountSettings.credentials(Credentials(certificateAlias = alias)) + Handler(Looper.getMainLooper()).post({ + loaderManager.restartLoader(0, arguments, this) + }) + }, null, null, null, -1, credentials.certificateAlias) + true + } + } } // preference group: sync diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt index d4e93d472..651f23754 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt @@ -62,10 +62,9 @@ class AccountDetailsFragment: Fragment(), LoaderManager.LoaderCallbacks - validateLoginData()?.let { credentials -> - DetectConfigurationFragment.newInstance(credentials).show(fragmentManager, null) + v.urlcert_select_cert.setOnClickListener { + val baseUrl = Uri.parse(view.urlcert_base_url.text.toString()) + KeyChain.choosePrivateKeyAlias(activity, KeyChainAliasCallback { alias -> + v.urlcert_cert_alias.text = alias + }, null, null, baseUrl.host, baseUrl.port,view.urlcert_cert_alias.text.toString()) + } + + v.login.setOnClickListener { + validateLoginData()?.let { info -> + DetectConfigurationFragment.newInstance(info).show(fragmentManager, null) } - }) + } // initialize to Login by email onCheckedChanged(v) v.login_type_email.setOnCheckedChangeListener(this) - v.login_type_url.setOnCheckedChangeListener(this) + v.login_type_urlpwd.setOnCheckedChangeListener(this) + v.login_type_urlcert.setOnCheckedChangeListener(this) return v } @@ -69,90 +79,120 @@ class DefaultLoginCredentialsFragment: Fragment(), CompoundButton.OnCheckedChang } private fun onCheckedChanged(v: View) { - val loginByEmail = !v.login_type_url.isChecked - v.login_type_email_details.visibility = if (loginByEmail) View.VISIBLE else View.GONE - v.login_type_url_details.visibility = if (loginByEmail) View.GONE else View.VISIBLE - (if (loginByEmail) v.email_address else v.base_url).requestFocus() + v.login_type_email_details.visibility = if (v.login_type_email.isChecked) View.VISIBLE else View.GONE + v.login_type_urlpwd_details.visibility = if (v.login_type_urlpwd.isChecked) View.VISIBLE else View.GONE + v.login_type_urlcert_details.visibility = if (v.login_type_urlcert.isChecked) View.VISIBLE else View.GONE } - private fun validateLoginData(): LoginCredentials? { - if (view.login_type_email.isChecked) { - var uri: URI? = null - var valid = true + private fun validateLoginData(): LoginInfo? { + when { + // Login with email address + view.login_type_email.isChecked -> { + var uri: URI? = null + var valid = true - val email = view.email_address.text.toString() - if (!email.matches(Regex(".+@.+"))) { - view.email_address.error = getString(R.string.login_email_address_error) - valid = false - } else - try { - uri = URI("mailto", email, null) - } catch(e: URISyntaxException) { - view.email_address.error = e.localizedMessage + val email = view.email_address.text.toString() + if (!email.matches(Regex(".+@.+"))) { + view.email_address.error = getString(R.string.login_email_address_error) + valid = false + } else + try { + uri = URI("mailto", email, null) + } catch (e: URISyntaxException) { + view.email_address.error = e.localizedMessage + valid = false + } + + val password = view.email_password.getText().toString() + if (password.isEmpty()) { + view.email_password.setError(getString(R.string.login_password_required)) valid = false } - val password = view.email_password.getText().toString() - if (password.isEmpty()) { - view.email_password.setError(getString(R.string.login_password_required)) - valid = false + return if (valid && uri != null) + LoginInfo(uri, email, password) + else + null + } - return if (valid && uri != null) - LoginCredentials(uri, email, password) - else - null - - } else if (view.login_type_url.isChecked) { - var uri: URI? = null - var valid = true - - val baseUrl = Uri.parse(view.base_url.text.toString()) - val scheme = baseUrl.scheme - if (scheme.equals("http", true) || scheme.equals("https", true)) { - var host = baseUrl.host - if (host.isNullOrBlank()) { - view.base_url.error = getString(R.string.login_url_host_name_required) + // Login with URL and user name + view.login_type_urlpwd.isChecked -> { + var valid = true + + val baseUrl = Uri.parse(view.urlpwd_base_url.text.toString()) + val uri = validateBaseUrl(baseUrl, false, { message -> + view.urlpwd_base_url.error = message valid = false - } else - try { - host = IDN.toASCII(host) - } catch(e: IllegalArgumentException) { - Constants.log.log(Level.WARNING, "Host name not conforming to RFC 3490", e) - } + }) - val path = baseUrl.encodedPath - val port = baseUrl.port - try { - uri = URI(baseUrl.scheme, null, host, port, path, null, null) - } catch(e: URISyntaxException) { - view.base_url.error = e.localizedMessage + val userName = view.urlpwd_user_name.text.toString() + if (userName.isBlank()) { + view.urlpwd_user_name.error = getString(R.string.login_user_name_required) valid = false } - } else { - view.base_url.error = getString(R.string.login_url_must_be_http_or_https) - valid = false - } - val userName = view.user_name.text.toString() - if (userName.isBlank()) { - view.user_name.error = getString(R.string.login_user_name_required) - valid = false - } + val password = view.urlpwd_password.getText().toString() + if (password.isEmpty()) { + view.urlpwd_password.setError(getString(R.string.login_password_required)) + valid = false + } - val password = view.url_password.getText().toString() - if (password.isEmpty()) { - view.url_password.setError(getString(R.string.login_password_required)) - valid = false + return if (valid && uri != null) + LoginInfo(uri, userName, password) + else + null } - return if (valid && uri != null) - LoginCredentials(uri, userName, password) - else - null + // Login with URL and client certificate + view.login_type_urlcert.isChecked -> { + var valid = true + + val baseUrl = Uri.parse(view.urlcert_base_url.text.toString()) + val uri = validateBaseUrl(baseUrl, true, { message -> + view.urlcert_base_url.error = message + valid = false + }) + + val alias = view.urlcert_cert_alias.text.toString() + if (alias.isEmpty()) + valid = false + + if (valid && uri != null) + return LoginInfo(uri, certificateAlias = alias) + } } return null } + private fun validateBaseUrl(baseUrl: Uri, httpsRequired: Boolean, reportError: (String) -> Unit): URI? { + var uri: URI? = null + val scheme = baseUrl.scheme + if ((!httpsRequired && scheme.equals("http", true)) || scheme.equals("https", true)) { + var host = baseUrl.host + if (host.isNullOrBlank()) + reportError(getString(R.string.login_url_host_name_required)) + else + try { + host = IDN.toASCII(host) + } catch (e: IllegalArgumentException) { + Constants.log.log(Level.WARNING, "Host name not conforming to RFC 3490", e) + } + + val path = baseUrl.encodedPath + val port = baseUrl.port + try { + uri = URI(baseUrl.scheme, null, host, port, path, null, null) + } catch (e: URISyntaxException) { + reportError(e.localizedMessage) + } + } else + reportError(getString(if (httpsRequired) + R.string.login_url_must_be_https + else + R.string.login_url_must_be_http_or_https)) + return uri + } + } diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/DetectConfigurationFragment.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/DetectConfigurationFragment.kt index 63b820571..d382775a0 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/DetectConfigurationFragment.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/DetectConfigurationFragment.kt @@ -23,7 +23,7 @@ class DetectConfigurationFragment: DialogFragment(), LoaderManager.LoaderCallbac companion object { val ARG_LOGIN_CREDENTIALS = "credentials" - fun newInstance(credentials: LoginCredentials): DetectConfigurationFragment { + fun newInstance(credentials: LoginInfo): DetectConfigurationFragment { val frag = DetectConfigurationFragment() val args = Bundle(1) args.putParcelable(ARG_LOGIN_CREDENTIALS, credentials) @@ -111,7 +111,7 @@ class DetectConfigurationFragment: DialogFragment(), LoaderManager.LoaderCallbac class ServerConfigurationLoader( context: Context, - private val credentials: LoginCredentials + private val credentials: LoginInfo ): AsyncTaskLoader(context) { var resourceFinder: DavResourceFinder? = null diff --git a/app/src/main/java/at/bitfire/davdroid/ui/setup/LoginCredentials.kt b/app/src/main/java/at/bitfire/davdroid/ui/setup/LoginInfo.kt similarity index 60% rename from app/src/main/java/at/bitfire/davdroid/ui/setup/LoginCredentials.kt rename to app/src/main/java/at/bitfire/davdroid/ui/setup/LoginInfo.kt index f573b9f1c..e2af5b04d 100644 --- a/app/src/main/java/at/bitfire/davdroid/ui/setup/LoginCredentials.kt +++ b/app/src/main/java/at/bitfire/davdroid/ui/setup/LoginInfo.kt @@ -10,34 +10,35 @@ package at.bitfire.davdroid.ui.setup import android.os.Parcel import android.os.Parcelable +import at.bitfire.davdroid.model.Credentials import java.net.URI -data class LoginCredentials( - val uri: URI, - val userName: String, - val password: String +data class LoginInfo( + @JvmField val uri: URI, + @JvmField val credentials: Credentials ): Parcelable { + constructor(uri: URI, userName: String? = null, password: String? = null, certificateAlias: String? = null): + this(uri, Credentials(userName, password, certificateAlias)) + override fun describeContents() = 0 override fun writeToParcel(dest: Parcel, flags: Int) { dest.writeSerializable(uri) - dest.writeString(userName) - dest.writeString(password) + dest.writeSerializable(credentials) } companion object { @JvmField - val CREATOR = object: Parcelable.Creator { + val CREATOR = object: Parcelable.Creator { override fun createFromParcel(source: Parcel) = - LoginCredentials( + LoginInfo( source.readSerializable() as URI, - source.readString(), - source.readString() + source.readSerializable() as Credentials ) - override fun newArray(size: Int) = arrayOfNulls(size) + override fun newArray(size: Int) = arrayOfNulls(size) } } diff --git a/app/src/main/res/layout/login_credentials_fragment.xml b/app/src/main/res/layout/login_credentials_fragment.xml index 3ea468967..743eacbcc 100644 --- a/app/src/main/res/layout/login_credentials_fragment.xml +++ b/app/src/main/res/layout/login_credentials_fragment.xml @@ -38,10 +38,10 @@ android:id="@+id/login_type_email" android:layout_width="match_parent" android:layout_height="wrap_content" + android:checked="true" android:text="@string/login_type_email" android:paddingLeft="14dp" tools:ignore="RtlSymmetry" style="@style/login_type_headline"/> - - + + + + + + +