From d415360d55f9379e36512cf0386921159c95b0f1 Mon Sep 17 00:00:00 2001 From: Fahim Salam Chowdhury Date: Mon, 12 Feb 2024 21:50:01 +0600 Subject: [PATCH 01/12] feat: replace nc-android-lib with eOS fork to support cookie passing on req, nc-andoir-lib should be replaced with eOS nc-android-lib fork --- app/build.gradle | 5 +++-- .../at/bitfire/davdroid/webdav/StreamingFileDescriptor.kt | 4 ++-- build.gradle | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 41d291c88..4385afa00 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,6 +9,7 @@ plugins { id 'dagger.hilt.android.plugin' id 'kotlin-android' id 'kotlin-kapt' // remove as soon as Hilt supports KSP [https://issuetracker.google.com/179057202] + id 'kotlin-parcelize' } // Android configuration @@ -28,7 +29,7 @@ android { minSdkVersion 24 // Android 7.0 targetSdkVersion 33 // Android 13 - buildConfigField "String", "userAgent", "\"DAVx5\"" + buildConfigField "String", "userAgent", "\"AccountManager\"" testInstrumentationRunner "at.bitfire.davdroid.CustomTestRunner" } @@ -217,7 +218,7 @@ dependencies { implementation "commons-httpclient:commons-httpclient:3.1@jar" // remove after entire switch to lib v2 implementation 'org.apache.jackrabbit:jackrabbit-webdav:2.13.5' // remove after entire switch to lib v2 implementation 'com.google.code.gson:gson:2.10.1' - implementation("com.github.nextcloud:android-library:2.14.0") { + implementation("foundation.e:Nextcloud-Android-Library:1.0.1-alpha") { exclude group: 'com.gitlab.bitfireAT', module: 'dav4jvm' exclude group: 'org.ogce', module: 'xpp3' // unused in Android and brings wrong Junit version exclude group: 'com.squareup.okhttp3' diff --git a/app/src/main/kotlin/at/bitfire/davdroid/webdav/StreamingFileDescriptor.kt b/app/src/main/kotlin/at/bitfire/davdroid/webdav/StreamingFileDescriptor.kt index 006b739d1..79ef169cf 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/webdav/StreamingFileDescriptor.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/webdav/StreamingFileDescriptor.kt @@ -21,7 +21,7 @@ import at.bitfire.davdroid.util.DavUtils import okhttp3.HttpUrl import okhttp3.MediaType import okhttp3.RequestBody -import okhttp3.internal.headersContentLength +import okhttp3.internal.toLongOrDefault import okio.BufferedSink import org.apache.commons.io.FileUtils import java.io.IOException @@ -107,7 +107,7 @@ class StreamingFileDescriptor( dav.get(mimeType?.toString() ?: DavUtils.MIME_TYPE_ACCEPT_ALL, null) { response -> response.body?.use { body -> if (response.isSuccessful) { - val length = response.headersContentLength() + val length = response.headers["Content-Length"]?.toLongOrDefault(-1L) ?: -1L notification.setContentTitle(context.getString(R.string.webdav_notification_download)) if (length == -1L) diff --git a/build.gradle b/build.gradle index de2cdb3e9..e7341354f 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ buildscript { hilt: '2.48.1', kotlin: '1.9.10', // keep in sync with * app/build.gradle composeOptions.kotlinCompilerExtensionVersion // * com.google.devtools.ksp at the end of this file - okhttp: '4.12.0', + okhttp: '5.0.0-alpha.11', room: '2.5.2', workManager: '2.9.0-rc01', // Apache Commons versions -- GitLab From 172366e67ed1ff741368cdfece9c9e807c72e82d Mon Sep 17 00:00:00 2001 From: Fahim Salam Chowdhury Date: Mon, 12 Feb 2024 22:08:11 +0600 Subject: [PATCH 02/12] feat: refactor SSO owncloud client creation logic - to pass cookie on req, SSO owncloudClient creation logic needs to be refactored - on account logout, owncloudClient needs to be removed from singleton map - on account login flow, need to pass valid userId & token for murena account --- app/src/main/AndroidManifest.xml | 11 ++- .../android/sso/InputStreamBinder.java | 40 +++++++---- .../activity/SsoGrantPermissionActivity.java | 21 +----- .../receiver/AccountRemovedReceiver.kt | 69 +++++++++++++++++++ .../{ => receiver}/BootCompletedReceiver.kt | 2 +- .../davdroid/settings/AccountSettings.kt | 13 +++- .../syncadapter/AccountsCleanupWorker.kt | 14 ++-- .../ui/setup/AccountDetailsFragment.kt | 14 ++++ .../at/bitfire/davdroid/util/SSOUtils.kt | 40 +++++++++++ 9 files changed, 179 insertions(+), 45 deletions(-) create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/receiver/AccountRemovedReceiver.kt rename app/src/main/kotlin/at/bitfire/davdroid/{ => receiver}/BootCompletedReceiver.kt (97%) create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/util/SSOUtils.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e519e3144..2039cf52d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -684,13 +684,22 @@ + + + + + + https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#2xx_Success if (status >= HTTP_STATUS_CODE_OK && status < HTTP_STATUS_CODE_MULTIPLE_CHOICES) { return method; @@ -380,6 +386,12 @@ public class InputStreamBinder extends IInputStreamService.Stub { } } + private static String getUserAgent() { + SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyMMdd", Locale.getDefault()); + String time = simpleDateFormat.format(Build.TIME); + return "eos(" + time + ")-AccountManager-SSO(" + BuildConfig.VERSION_NAME + ")"; + } + private Response processRequestV2(final NextcloudRequest request, final InputStream requestBodyInputStream) throws UnsupportedOperationException, com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException, @@ -400,9 +412,10 @@ public class InputStreamBinder extends IInputStreamService.Stub { new IllegalStateException("URL need to start with a /")); } - Uri serverUri = Uri.parse(AccountUtils.getBaseUrlForAccount(context, account)); - OwnCloudClient client = OwnCloudClientFactory.createOwnCloudClient(serverUri, context, true); - client.setCredentials(OwnCloudCredentialsFactory.newBasicCredentials(account.name, getAcountPwd(account, context))); + OwnCloudClientManagerFactory.setUserAgent(getUserAgent()); + OwnCloudClientManager ownCloudClientManager = OwnCloudClientManagerFactory.getDefaultSingleton(); + OwnCloudAccount ocAccount = new OwnCloudAccount(account, context); + OwnCloudClient client = ownCloudClientManager.getClientFor(ocAccount, context); HttpMethodBase method = buildMethod(request, client.getBaseUri(), requestBodyInputStream); @@ -428,6 +441,8 @@ public class InputStreamBinder extends IInputStreamService.Stub { client.setFollowRedirects(true); int status = client.executeMethod(method); + ownCloudClientManager.saveAllClients(context, account.type); + // Check if status code is 2xx --> https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#2xx_Success if (status >= HTTP_STATUS_CODE_OK && status < HTTP_STATUS_CODE_MULTIPLE_CHOICES) { return new Response(method); @@ -447,11 +462,6 @@ public class InputStreamBinder extends IInputStreamService.Stub { } } - private static String getAcountPwd(Account account, Context ctx) throws AccountUtils.AccountNotFoundException { - return AccountManager.get(ctx).getPassword(account); - } - - private boolean isValid(NextcloudRequest request) { String callingPackageName = context.getPackageManager().getNameForUid(Binder.getCallingUid()); diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SsoGrantPermissionActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/SsoGrantPermissionActivity.java index 5b0607f53..4c61a25b5 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SsoGrantPermissionActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/SsoGrantPermissionActivity.java @@ -35,7 +35,6 @@ import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; @@ -49,6 +48,7 @@ import java.util.logging.Level; import at.bitfire.davdroid.R; import at.bitfire.davdroid.log.Logger; +import at.bitfire.davdroid.util.SSOUtils; public class SsoGrantPermissionActivity extends AppCompatActivity { @@ -115,7 +115,7 @@ public class SsoGrantPermissionActivity extends AppCompatActivity { // create token String token = UUID.randomUUID().toString().replaceAll("-", ""); - String userId = sanitizeUserId(account.name); + String userId = SSOUtils.INSTANCE.sanitizeUserId(account.name); saveToken(token, account.name); setResultData(token, userId, serverUrl); @@ -136,23 +136,6 @@ public class SsoGrantPermissionActivity extends AppCompatActivity { setResult(RESULT_OK, data); } - /** - * Murena account's userId is set same as it's email address. - * For old accounts (@e.email) userId = email. - * For new accounts (@murena.io) userId is first part of email (ex: for email abc@murena.io, userId is abc). - * For api requests, we needed to pass the actual userId. This method remove the unwanted part (@murena.io) from the userId - */ - @NonNull - private static String sanitizeUserId(@NonNull String userId) { - final String murenaMailEndPart = "@murena.io"; - - if (!userId.endsWith(murenaMailEndPart)) { - return userId; - } - - return userId.split(murenaMailEndPart)[0]; - } - @Nullable private String getServerUrl() { try { diff --git a/app/src/main/kotlin/at/bitfire/davdroid/receiver/AccountRemovedReceiver.kt b/app/src/main/kotlin/at/bitfire/davdroid/receiver/AccountRemovedReceiver.kt new file mode 100644 index 000000000..743302e53 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/receiver/AccountRemovedReceiver.kt @@ -0,0 +1,69 @@ +/* + * Copyright MURENA SAS 2024 + * This program 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. + * + * This program 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 this program. If not, see . + */ + +package at.bitfire.davdroid.receiver + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import at.bitfire.davdroid.log.Logger +import at.bitfire.davdroid.syncadapter.AccountUtils +import com.owncloud.android.lib.common.OwnCloudAccount +import com.owncloud.android.lib.common.OwnCloudClientManagerFactory +import com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException + +class AccountRemovedReceiver : BroadcastReceiver() { + + companion object { + private const val ACCOUNT_REMOVAL_ACTION = "android.accounts.action.ACCOUNT_REMOVED" + } + + override fun onReceive(context: Context?, intent: Intent?) { + if (context == null || intent == null || intent.action != ACCOUNT_REMOVAL_ACTION) { + return + } + + val account = getAccount(context, intent) ?: return + + val ownCloudClientManager = OwnCloudClientManagerFactory.getDefaultSingleton() + try { + val ocAccount = OwnCloudAccount(account, context) + ownCloudClientManager.removeClientFor(ocAccount) + } catch (e: AccountNotFoundException) { + Logger.log.warning("exception thrown as account not found. Mostly because not NC account. ${e.localizedMessage}") + } + } + + private fun getAccount(context: Context, intent: Intent): Account? { + val accountType = intent.extras?.getString(AccountManager.KEY_ACCOUNT_TYPE) + if (accountType !in AccountUtils.getMainAccountTypes(context)) { + return null + } + + val accountName = intent.extras?.getString(AccountManager.KEY_ACCOUNT_NAME) ?: return null + + val accounts = AccountManager.get(context).getAccountsByType(accountType) + for (account in accounts) { + if (account.name == accountName) { + return account + } + } + + return null + } +} diff --git a/app/src/main/kotlin/at/bitfire/davdroid/BootCompletedReceiver.kt b/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt similarity index 97% rename from app/src/main/kotlin/at/bitfire/davdroid/BootCompletedReceiver.kt rename to app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt index dbbcd89ad..d76975081 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/BootCompletedReceiver.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt @@ -2,7 +2,7 @@ * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. **************************************************************************************************/ -package at.bitfire.davdroid +package at.bitfire.davdroid.receiver import android.content.BroadcastReceiver import android.content.Context diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt index 4bd79bbd2..198e6913d 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt @@ -19,6 +19,7 @@ import at.bitfire.davdroid.resource.LocalAddressBook import at.bitfire.davdroid.syncadapter.AccountUtils import at.bitfire.davdroid.syncadapter.PeriodicSyncWorker import at.bitfire.davdroid.syncadapter.SyncUtils +import at.bitfire.davdroid.util.SSOUtils import at.bitfire.davdroid.util.setAndVerifyUserData import at.bitfire.ical4android.TaskProvider import at.bitfire.vcard4android.GroupMethod @@ -29,6 +30,8 @@ import dagger.hilt.components.SingletonComponent import net.openid.appauth.AuthState import org.apache.commons.lang3.StringUtils import java.util.logging.Level +import com.owncloud.android.lib.common.accounts.AccountUtils as NCAccountUtils + /** * Manages settings of an account. @@ -115,6 +118,9 @@ class AccountSettings( const val CONTACTS_APP_INTERACTION = "z-app-generated--contactsinteraction--recent/" + const val MURENA_COOKIE_KEY = "murena_cookie_key" + const val COOKIE_SEPARATOR = "" + /** Static property to indicate whether AccountSettings migration is currently running. * **Access must be `synchronized` with `AccountSettings::class.java`.** */ @Volatile @@ -127,11 +133,14 @@ class AccountSettings( if (credentials != null) { if (credentials.userName != null) { bundle.putString(KEY_USERNAME, credentials.userName) - bundle.putString("oc_display_name", credentials.userName) + bundle.putString(NCAccountUtils.Constants.KEY_DISPLAY_NAME, credentials.userName) if (credentials.userName.contains("@")) { bundle.putString(KEY_EMAIL_ADDRESS, credentials.userName) } + + val userId = SSOUtils.sanitizeUserId(credentials.userName) + bundle.putString(NCAccountUtils.Constants.KEY_USER_ID, userId) } if (credentials.certificateAlias != null) { @@ -148,7 +157,7 @@ class AccountSettings( } if (!baseURL.isNullOrEmpty()) { - bundle.putString("oc_base_url", getOCBaseUrl(baseURL)) + bundle.putString(NCAccountUtils.Constants.KEY_OC_BASE_URL, getOCBaseUrl(baseURL)) } return bundle diff --git a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountsCleanupWorker.kt b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountsCleanupWorker.kt index 59e827633..cbf740f65 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountsCleanupWorker.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountsCleanupWorker.kt @@ -4,12 +4,13 @@ package at.bitfire.davdroid.syncadapter -import android.accounts.Account -import android.accounts.AccountManager import android.content.Context import androidx.hilt.work.HiltWorker -import androidx.work.* -import at.bitfire.davdroid.R +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.Worker +import androidx.work.WorkerParameters import at.bitfire.davdroid.db.AppDatabase import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.resource.LocalAddressBook @@ -49,15 +50,14 @@ class AccountsCleanupWorker @AssistedInject constructor( override fun doWork(): Result { lockAccountsCleanup() try { - val accountManager = AccountManager.get(applicationContext) - cleanupAccounts(applicationContext, accountManager.accounts) + cleanupAccounts(applicationContext) } finally { unlockAccountsCleanup() } return Result.success() } - private fun cleanupAccounts(context: Context, accounts: Array) { + private fun cleanupAccounts(context: Context) { Logger.log.log(Level.INFO, "Cleaning up accounts. Current accounts") val mainAccountNames = HashSet() diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt index 29543cd06..6b08ce212 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt @@ -49,6 +49,8 @@ import at.bitfire.davdroid.syncadapter.SyncAllAccountWorker import at.bitfire.davdroid.syncadapter.SyncWorker import at.bitfire.vcard4android.GroupMethod import com.google.android.material.snackbar.Snackbar +import com.nextcloud.android.utils.AccountManagerUtils +import com.owncloud.android.lib.common.accounts.AccountTypeUtils import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers @@ -315,10 +317,22 @@ class AccountDetailsFragment : Fragment() { accountManager.setAuthToken(account, Constants.AUTH_TOKEN_TYPE, credentials?.authState?.accessToken) } + var pass: String? = null + if (!credentials?.password.isNullOrEmpty()) { + pass = credentials?.password + } + + pass?.let { + if (accountType == AccountManagerUtils.getAccountType(context)) { + accountManager.setAuthToken(account, AccountTypeUtils.getAuthTokenTypePass(account.type), it) + return@let + } + accountManager.setPassword(account, credentials?.password) } + ContentResolver.setSyncAutomatically(account, context.getString(R.string.notes_authority), true) ContentResolver.setSyncAutomatically(account, context.getString(R.string.email_authority), true) ContentResolver.setSyncAutomatically(account, context.getString(R.string.media_authority), true) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/util/SSOUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/util/SSOUtils.kt new file mode 100644 index 000000000..9ba2077da --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/util/SSOUtils.kt @@ -0,0 +1,40 @@ +/* + * Copyright MURENA SAS 2024 + * This program 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. + * + * This program 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 this program. If not, see . + */ + +package at.bitfire.davdroid.util + +object SSOUtils { + /** + * Murena account's userId is set same as it's email address. + * For old accounts (@e.email) userId = email. + * For new accounts (@murena.io) & other NC accounts userId is first part of email (ex: for email abc@murena.io, userId is abc). + * For api requests, we needed to pass the actual userId. This method remove the unwanted part (ex: @murena.io) from the userId + */ + fun sanitizeUserId(userId: String): String { + val eeloMailEndPart = "@e.email" + + if (userId.endsWith(eeloMailEndPart)) { + return userId + } + + if (userId.lastIndexOf("@") < 0) { // not email address + return userId + } + + return userId.split("@").dropLastWhile { it.isEmpty() } + .toTypedArray()[0] + } +} -- GitLab From e5c9601144938eef1fc6accc5673f40d10728753 Mon Sep 17 00:00:00 2001 From: Fahim Salam Chowdhury Date: Mon, 12 Feb 2024 22:09:48 +0600 Subject: [PATCH 03/12] feat: impl murena cookie store to pass & reserve cookies for DAV requests of murena account --- .../davdroid/network/CookieStoreFactory.kt | 37 +++++++++ .../at/bitfire/davdroid/network/HttpClient.kt | 2 + .../davdroid/network/MurenaCookieStore.kt | 76 +++++++++++++++++++ 3 files changed, 115 insertions(+) create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/network/CookieStoreFactory.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/network/MurenaCookieStore.kt diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/CookieStoreFactory.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/CookieStoreFactory.kt new file mode 100644 index 000000000..650b24688 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/network/CookieStoreFactory.kt @@ -0,0 +1,37 @@ +/* + * Copyright MURENA SAS 2024 + * This program 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. + * + * This program 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 this program. If not, see . + */ + +package at.bitfire.davdroid.network + +import android.accounts.Account +import android.content.Context +import at.bitfire.davdroid.R +import okhttp3.CookieJar + +fun createCookieStore(context: Context? = null, account: Account? = null): CookieJar { + if (context == null || account == null) { + return MemoryCookieStore() + } + + val murenaAccountType = context.getString(R.string.eelo_account_type) + val murenaAddressBookAccountType = context.getString(R.string.account_type_eelo_address_book) + + if (account.type in listOf(murenaAccountType, murenaAddressBookAccountType)) { + return MurenaCookieStore(context, account) + } + + return MemoryCookieStore() +} diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/HttpClient.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/HttpClient.kt index 5c38b9841..f0f5dd3a5 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/network/HttpClient.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/network/HttpClient.kt @@ -175,6 +175,8 @@ class HttpClient private constructor( authStateCallback = { authState: AuthState -> updateCredentials(accountSettings, authState, credential.clientSecret) }) + + cookieStore = createCookieStore(context, accountSettings.account) } private fun updateCredentials( diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/MurenaCookieStore.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/MurenaCookieStore.kt new file mode 100644 index 000000000..8c811728e --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/network/MurenaCookieStore.kt @@ -0,0 +1,76 @@ +/* + * Copyright MURENA SAS 2024 + * This program 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. + * + * This program 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 this program. If not, see . + */ + +package at.bitfire.davdroid.network + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.Context +import at.bitfire.davdroid.settings.AccountSettings +import okhttp3.Cookie +import okhttp3.CookieJar +import okhttp3.HttpUrl + +class MurenaCookieStore(context: Context, private val account: Account): CookieJar { + + private val accountManager = AccountManager.get(context.applicationContext) + + override fun loadForRequest(url: HttpUrl): List { + return getCookieMap(url).values.filter { + it.matches(url) + } + } + + override fun saveFromResponse(url: HttpUrl, cookies: List) { + val cookieList = cookies.filter { + it.expiresAt > System.currentTimeMillis() + } + + if (cookieList.isEmpty()) { + return + } + + val cookieMap = getCookieMap(url) + + // replace old cookie with new one + cookieList.forEach { + cookieMap[it.name] = it + } + + val cookieString = cookieMap.values.joinToString(separator = AccountSettings.COOKIE_SEPARATOR) + accountManager.setUserData(account, AccountSettings.MURENA_COOKIE_KEY, cookieString) + } + + private fun getCookieMap(url: HttpUrl): HashMap { + val result = HashMap() + + val cookiesString = accountManager.getUserData(account, AccountSettings.MURENA_COOKIE_KEY) + ?: return HashMap() + + val cookies = cookiesString.split(AccountSettings.COOKIE_SEPARATOR.toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray() + + cookies.forEach { + val cookie = Cookie.parse(url, it) ?: return@forEach + + if (cookie.expiresAt > System.currentTimeMillis()) { + result[cookie.name] = cookie + } + } + + return result + } +} -- GitLab From 6eb995b47bebfab5bf5e1421e2263de27d6a5399 Mon Sep 17 00:00:00 2001 From: Fahim Salam Chowdhury Date: Tue, 13 Feb 2024 15:30:21 +0600 Subject: [PATCH 04/12] fix: add missing proper cookie setup for some requests --- .../at/bitfire/davdroid/network/HttpClient.kt | 11 +++++-- .../davdroid/settings/AccountSettings.kt | 10 +------ .../davdroid/syncadapter/AccountUtils.kt | 30 +++++++++++++++++++ 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/HttpClient.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/HttpClient.kt index f0f5dd3a5..af6ba1205 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/network/HttpClient.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/network/HttpClient.kt @@ -4,6 +4,7 @@ package at.bitfire.davdroid.network +import android.accounts.Account import android.content.Context import android.os.Build import android.security.KeyChain @@ -17,6 +18,7 @@ import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.settings.Settings import at.bitfire.davdroid.settings.SettingsManager +import at.bitfire.davdroid.syncadapter.AccountUtils import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.android.EntryPointAccessors @@ -172,11 +174,10 @@ class HttpClient private constructor( addAuthentication( null, credential, + account = accountSettings.account, authStateCallback = { authState: AuthState -> updateCredentials(accountSettings, authState, credential.clientSecret) }) - - cookieStore = createCookieStore(context, accountSettings.account) } private fun updateCredentials( @@ -192,7 +193,7 @@ class HttpClient private constructor( ) } - fun addAuthentication(host: String?, credentials: Credentials, insecurePreemptive: Boolean = false, authStateCallback: BearerAuthInterceptor.AuthStateUpdateCallback? = null): Builder { + fun addAuthentication(host: String?, credentials: Credentials, insecurePreemptive: Boolean = false, account: Account? = null, authStateCallback: BearerAuthInterceptor.AuthStateUpdateCallback? = null): Builder { if (credentials.userName != null && credentials.password != null) { val authHandler = BasicDigestAuthHandler(UrlUtils.hostToDomain(host), credentials.userName, credentials.password, insecurePreemptive) orig.addNetworkInterceptor(authHandler) @@ -210,6 +211,10 @@ class HttpClient private constructor( orig.addNetworkInterceptor(bearerAuthInterceptor) } } + + val accountForCookie = account ?: AccountUtils.getAccount(context, credentials.userName, host) + cookieStore = createCookieStore(context, accountForCookie) + return this } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt index 198e6913d..0f7ab6b5a 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt @@ -157,20 +157,12 @@ class AccountSettings( } if (!baseURL.isNullOrEmpty()) { - bundle.putString(NCAccountUtils.Constants.KEY_OC_BASE_URL, getOCBaseUrl(baseURL)) + bundle.putString(NCAccountUtils.Constants.KEY_OC_BASE_URL, AccountUtils.getOwnCloudBaseUrl(baseURL)) } return bundle } - private fun getOCBaseUrl(baseURL: String): String { - if (baseURL.contains("remote.php")) { - return baseURL.split("/remote.php")[0] - } - - return baseURL - } - } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt index 5dcaed9c6..fad00f540 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt @@ -11,6 +11,7 @@ import android.os.Bundle import at.bitfire.davdroid.R import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.util.setAndVerifyUserData +import com.owncloud.android.lib.common.accounts.AccountUtils object AccountUtils { @@ -128,4 +129,33 @@ object AccountUtils { return accounts } + + fun getOwnCloudBaseUrl(baseURL: String): String { + if (baseURL.contains("remote.php")) { + return baseURL.split("/remote.php")[0] + } + + return baseURL + } + + fun getAccount(context: Context, userName: String?, requestedBaseUrl: String?): Account? { + if (userName == null || requestedBaseUrl == null) { + return null; + } + + val baseUrl = getOwnCloudBaseUrl(requestedBaseUrl) + + val accountManager = AccountManager.get(context.applicationContext) + + val accounts = getMainAccounts(context) + + for(account in accounts) { + val url = accountManager.getUserData(account, AccountUtils.Constants.KEY_OC_BASE_URL) + if (url != null && getOwnCloudBaseUrl(url) == baseUrl) { + return account + } + } + + return null + } } -- GitLab From 24a31235c82963301c5bc2e250aa1f7b509deb70 Mon Sep 17 00:00:00 2001 From: Fahim Salam Chowdhury Date: Tue, 13 Feb 2024 21:14:05 +0600 Subject: [PATCH 05/12] fix: Session cookie passing after successful resource finder When user init the login flow, first app tries to retrieve the DAV providers info by making DAV request via DavResourceFinder. In this stage, no account is created, so inMemoryCookieStore is used. After successful resource info retrieval then account is created & then refreshed the collection via RefreshCollectionWorker. As previously only inMemory cookieJar is used, the refreshCollection makes it's first request without any cookie, which causes extra server headace. To mitigate this, we need to persist the last inMemory cookies after successful resource retrieval. --- .../bitfire/davdroid/network/CookieParser.kt | 22 +++++++++++++++++++ .../davdroid/network/CookieStoreFactory.kt | 2 +- .../at/bitfire/davdroid/network/HttpClient.kt | 8 +++++++ .../davdroid/network/MemoryCookieStore.kt | 13 +++++++++-- ...okieStore.kt => PersistenceCookieStore.kt} | 6 ++--- .../servicedetection/DavResourceFinder.kt | 6 +++-- .../davdroid/settings/AccountSettings.kt | 8 +++++-- .../davdroid/syncadapter/AccountUtils.kt | 6 +++++ .../ui/setup/AccountDetailsFragment.kt | 2 +- 9 files changed, 62 insertions(+), 11 deletions(-) create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/network/CookieParser.kt rename app/src/main/kotlin/at/bitfire/davdroid/network/{MurenaCookieStore.kt => PersistenceCookieStore.kt} (91%) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/CookieParser.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/CookieParser.kt new file mode 100644 index 000000000..87960a269 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/network/CookieParser.kt @@ -0,0 +1,22 @@ +/* + * Copyright MURENA SAS 2024 + * This program 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. + * + * This program 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 this program. If not, see . + */ + +package at.bitfire.davdroid.network + +interface CookieParser { + + fun cookiesAsString(): String +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/CookieStoreFactory.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/CookieStoreFactory.kt index 650b24688..fe5c4b058 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/network/CookieStoreFactory.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/network/CookieStoreFactory.kt @@ -30,7 +30,7 @@ fun createCookieStore(context: Context? = null, account: Account? = null): Cooki val murenaAddressBookAccountType = context.getString(R.string.account_type_eelo_address_book) if (account.type in listOf(murenaAccountType, murenaAddressBookAccountType)) { - return MurenaCookieStore(context, account) + return PersistenceCookieStore(context, account) } return MemoryCookieStore() diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/HttpClient.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/HttpClient.kt index af6ba1205..ac47042b3 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/network/HttpClient.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/network/HttpClient.kt @@ -347,4 +347,12 @@ class HttpClient private constructor( } + fun getCookieAsString(): String { + val cookieJar = okHttpClient.cookieJar + if (cookieJar is CookieParser) { + return cookieJar.cookiesAsString() + } + + return "" + } } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/MemoryCookieStore.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/MemoryCookieStore.kt index 05e7ea63f..3eacb5ce1 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/network/MemoryCookieStore.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/network/MemoryCookieStore.kt @@ -4,19 +4,20 @@ package at.bitfire.davdroid.network +import at.bitfire.davdroid.settings.AccountSettings import okhttp3.Cookie import okhttp3.CookieJar import okhttp3.HttpUrl import org.apache.commons.collections4.keyvalue.MultiKey import org.apache.commons.collections4.map.HashedMap import org.apache.commons.collections4.map.MultiKeyMap -import java.util.* +import java.util.LinkedList /** * Primitive cookie store that stores cookies in a (volatile) hash map. * Will be sufficient for session cookies. */ -class MemoryCookieStore: CookieJar { +class MemoryCookieStore : CookieJar, CookieParser { /** * Stored cookies. The multi-key consists of three parts: name, domain, and path. @@ -56,4 +57,12 @@ class MemoryCookieStore: CookieJar { return cookies } + override fun cookiesAsString(): String { + if (storage.isEmpty) { + return "" + } + + return storage.values.joinToString(separator = AccountSettings.COOKIE_SEPARATOR) + } + } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/MurenaCookieStore.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/PersistenceCookieStore.kt similarity index 91% rename from app/src/main/kotlin/at/bitfire/davdroid/network/MurenaCookieStore.kt rename to app/src/main/kotlin/at/bitfire/davdroid/network/PersistenceCookieStore.kt index 8c811728e..f6c817567 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/network/MurenaCookieStore.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/network/PersistenceCookieStore.kt @@ -24,7 +24,7 @@ import okhttp3.Cookie import okhttp3.CookieJar import okhttp3.HttpUrl -class MurenaCookieStore(context: Context, private val account: Account): CookieJar { +class PersistenceCookieStore(context: Context, private val account: Account): CookieJar { private val accountManager = AccountManager.get(context.applicationContext) @@ -51,13 +51,13 @@ class MurenaCookieStore(context: Context, private val account: Account): CookieJ } val cookieString = cookieMap.values.joinToString(separator = AccountSettings.COOKIE_SEPARATOR) - accountManager.setUserData(account, AccountSettings.MURENA_COOKIE_KEY, cookieString) + accountManager.setUserData(account, AccountSettings.COOKIE_KEY, cookieString) } private fun getCookieMap(url: HttpUrl): HashMap { val result = HashMap() - val cookiesString = accountManager.getUserData(account, AccountSettings.MURENA_COOKIE_KEY) + val cookiesString = accountManager.getUserData(account, AccountSettings.COOKIE_KEY) ?: return HashMap() val cookies = cookiesString.split(AccountSettings.COOKIE_SEPARATOR.toRegex()).dropLastWhile { it.isEmpty() } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/DavResourceFinder.kt b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/DavResourceFinder.kt index 72f8cba8b..befcb3458 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/DavResourceFinder.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/DavResourceFinder.kt @@ -111,7 +111,8 @@ class DavResourceFinder( return Configuration( cardDavConfig, calDavConfig, encountered401, - logBuffer.toString() + logBuffer.toString(), + cookies = httpClient.getCookieAsString() ) } @@ -500,7 +501,8 @@ class DavResourceFinder( val calDAV: ServiceInfo?, val encountered401: Boolean, - val logs: String + val logs: String, + val cookies: String? = null ) { data class ServiceInfo( diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt index 0f7ab6b5a..e9349e631 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt @@ -118,7 +118,7 @@ class AccountSettings( const val CONTACTS_APP_INTERACTION = "z-app-generated--contactsinteraction--recent/" - const val MURENA_COOKIE_KEY = "murena_cookie_key" + const val COOKIE_KEY = "cookie_key" const val COOKIE_SEPARATOR = "" /** Static property to indicate whether AccountSettings migration is currently running. @@ -126,7 +126,7 @@ class AccountSettings( @Volatile var currentlyUpdating = false - fun initialUserData(credentials: Credentials?, baseURL: String? = null): Bundle { + fun initialUserData(credentials: Credentials?, baseURL: String? = null, cookies: String? = null): Bundle { val bundle = Bundle() bundle.putString(KEY_SETTINGS_VERSION, CURRENT_VERSION.toString()) @@ -160,6 +160,10 @@ class AccountSettings( bundle.putString(NCAccountUtils.Constants.KEY_OC_BASE_URL, AccountUtils.getOwnCloudBaseUrl(baseURL)) } + if (!cookies.isNullOrEmpty()) { + bundle.putString(COOKIE_KEY, cookies) + } + return bundle } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt index fad00f540..d364f4188 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt @@ -10,6 +10,7 @@ import android.content.Context import android.os.Bundle import at.bitfire.davdroid.R import at.bitfire.davdroid.log.Logger +import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.util.setAndVerifyUserData import com.owncloud.android.lib.common.accounts.AccountUtils @@ -150,6 +151,11 @@ object AccountUtils { val accounts = getMainAccounts(context) for(account in accounts) { + val name = accountManager.getUserData(account, AccountSettings.KEY_USERNAME) + if (name != userName) { + continue + } + val url = accountManager.getUserData(account, AccountUtils.Constants.KEY_OC_BASE_URL) if (url != null && getOwnCloudBaseUrl(url) == baseUrl) { return account diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt index 6b08ce212..ced0c5c1e 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/AccountDetailsFragment.kt @@ -299,7 +299,7 @@ class AccountDetailsFragment : Fragment() { val account = Account(name, accountType) // create Android account - val userData = AccountSettings.initialUserData(credentials, baseURL) + val userData = AccountSettings.initialUserData(credentials, baseURL, config.cookies) Logger.log.log(Level.INFO, "Creating Android account with initial config", arrayOf(account, userData)) val accountManager = AccountManager.get(context) -- GitLab From b2dc6b6db95f3888ca18fffb9504b489547ce3f5 Mon Sep 17 00:00:00 2001 From: Fahim Salam Chowdhury Date: Wed, 14 Feb 2024 12:12:42 +0600 Subject: [PATCH 06/12] fix: ocClient is not clearing from cache after logout As AccountRemovedReceiver is called after the account removed, the account is not retrieving in the receiver, so client is not clearing. To resove this, we can now remove the client via the account name only. --- app/build.gradle | 2 +- .../receiver/AccountRemovedReceiver.kt | 26 +++---------------- 2 files changed, 5 insertions(+), 23 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 4385afa00..4d325d769 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -218,7 +218,7 @@ dependencies { implementation "commons-httpclient:commons-httpclient:3.1@jar" // remove after entire switch to lib v2 implementation 'org.apache.jackrabbit:jackrabbit-webdav:2.13.5' // remove after entire switch to lib v2 implementation 'com.google.code.gson:gson:2.10.1' - implementation("foundation.e:Nextcloud-Android-Library:1.0.1-alpha") { + implementation("foundation.e:Nextcloud-Android-Library:1.0.5-alpha") { exclude group: 'com.gitlab.bitfireAT', module: 'dav4jvm' exclude group: 'org.ogce', module: 'xpp3' // unused in Android and brings wrong Junit version exclude group: 'com.squareup.okhttp3' diff --git a/app/src/main/kotlin/at/bitfire/davdroid/receiver/AccountRemovedReceiver.kt b/app/src/main/kotlin/at/bitfire/davdroid/receiver/AccountRemovedReceiver.kt index 743302e53..12be4afe5 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/receiver/AccountRemovedReceiver.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/receiver/AccountRemovedReceiver.kt @@ -16,16 +16,12 @@ package at.bitfire.davdroid.receiver -import android.accounts.Account import android.accounts.AccountManager import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.syncadapter.AccountUtils -import com.owncloud.android.lib.common.OwnCloudAccount import com.owncloud.android.lib.common.OwnCloudClientManagerFactory -import com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException class AccountRemovedReceiver : BroadcastReceiver() { @@ -38,32 +34,18 @@ class AccountRemovedReceiver : BroadcastReceiver() { return } - val account = getAccount(context, intent) ?: return + val accountName = getAccountName(context, intent) ?: return val ownCloudClientManager = OwnCloudClientManagerFactory.getDefaultSingleton() - try { - val ocAccount = OwnCloudAccount(account, context) - ownCloudClientManager.removeClientFor(ocAccount) - } catch (e: AccountNotFoundException) { - Logger.log.warning("exception thrown as account not found. Mostly because not NC account. ${e.localizedMessage}") - } + ownCloudClientManager.removeClientForByName(accountName) } - private fun getAccount(context: Context, intent: Intent): Account? { + private fun getAccountName(context: Context, intent: Intent): String? { val accountType = intent.extras?.getString(AccountManager.KEY_ACCOUNT_TYPE) if (accountType !in AccountUtils.getMainAccountTypes(context)) { return null } - val accountName = intent.extras?.getString(AccountManager.KEY_ACCOUNT_NAME) ?: return null - - val accounts = AccountManager.get(context).getAccountsByType(accountType) - for (account in accounts) { - if (account.name == accountName) { - return account - } - } - - return null + return intent.extras?.getString(AccountManager.KEY_ACCOUNT_NAME) } } -- GitLab From eb532185bb3dfe6bd9589ab25c67b11db09bc66d Mon Sep 17 00:00:00 2001 From: Fahim Salam Chowdhury Date: Thu, 15 Feb 2024 12:39:42 +0600 Subject: [PATCH 07/12] chore: update nc-android-lib version --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 4d325d769..bb3c67378 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -218,7 +218,7 @@ dependencies { implementation "commons-httpclient:commons-httpclient:3.1@jar" // remove after entire switch to lib v2 implementation 'org.apache.jackrabbit:jackrabbit-webdav:2.13.5' // remove after entire switch to lib v2 implementation 'com.google.code.gson:gson:2.10.1' - implementation("foundation.e:Nextcloud-Android-Library:1.0.5-alpha") { + implementation("foundation.e:Nextcloud-Android-Library:1.0.5-release") { exclude group: 'com.gitlab.bitfireAT', module: 'dav4jvm' exclude group: 'org.ogce', module: 'xpp3' // unused in Android and brings wrong Junit version exclude group: 'com.squareup.okhttp3' -- GitLab From 2e7b0c8b3281f28224fad624484540893ccbb087 Mon Sep 17 00:00:00 2001 From: Fahim Salam Chowdhury Date: Tue, 20 Feb 2024 10:28:33 +0600 Subject: [PATCH 08/12] chore: Improve naming for PersistentCookieStore class --- .../kotlin/at/bitfire/davdroid/network/CookieStoreFactory.kt | 2 +- .../{PersistenceCookieStore.kt => PersistentCookieStore.kt} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename app/src/main/kotlin/at/bitfire/davdroid/network/{PersistenceCookieStore.kt => PersistentCookieStore.kt} (96%) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/CookieStoreFactory.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/CookieStoreFactory.kt index fe5c4b058..fa1603e9f 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/network/CookieStoreFactory.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/network/CookieStoreFactory.kt @@ -30,7 +30,7 @@ fun createCookieStore(context: Context? = null, account: Account? = null): Cooki val murenaAddressBookAccountType = context.getString(R.string.account_type_eelo_address_book) if (account.type in listOf(murenaAccountType, murenaAddressBookAccountType)) { - return PersistenceCookieStore(context, account) + return PersistentCookieStore(context, account) } return MemoryCookieStore() diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/PersistenceCookieStore.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/PersistentCookieStore.kt similarity index 96% rename from app/src/main/kotlin/at/bitfire/davdroid/network/PersistenceCookieStore.kt rename to app/src/main/kotlin/at/bitfire/davdroid/network/PersistentCookieStore.kt index f6c817567..6170ea2db 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/network/PersistenceCookieStore.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/network/PersistentCookieStore.kt @@ -24,7 +24,7 @@ import okhttp3.Cookie import okhttp3.CookieJar import okhttp3.HttpUrl -class PersistenceCookieStore(context: Context, private val account: Account): CookieJar { +class PersistentCookieStore(context: Context, private val account: Account): CookieJar { private val accountManager = AccountManager.get(context.applicationContext) -- GitLab From a1ae49a17c847d93145f5fa7afe490fa948bb780 Mon Sep 17 00:00:00 2001 From: Fahim Salam Chowdhury Date: Tue, 20 Feb 2024 14:45:56 +0600 Subject: [PATCH 09/12] chore: remove extra context == null check --- .../kotlin/at/bitfire/davdroid/network/CookieStoreFactory.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/CookieStoreFactory.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/CookieStoreFactory.kt index fa1603e9f..2ae293420 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/network/CookieStoreFactory.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/network/CookieStoreFactory.kt @@ -21,8 +21,8 @@ import android.content.Context import at.bitfire.davdroid.R import okhttp3.CookieJar -fun createCookieStore(context: Context? = null, account: Account? = null): CookieJar { - if (context == null || account == null) { +fun createCookieStore(context: Context, account: Account? = null): CookieJar { + if (account == null) { return MemoryCookieStore() } -- GitLab From eb400776c19c2ae39c062ea2fbc99ebd553a1fd7 Mon Sep 17 00:00:00 2001 From: Fahim Salam Chowdhury Date: Tue, 20 Feb 2024 15:21:39 +0600 Subject: [PATCH 10/12] chore: improve the AccountManager-SSO userAgent name --- .../java/com/nextcloud/android/sso/InputStreamBinder.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/app/src/main/java/com/nextcloud/android/sso/InputStreamBinder.java b/app/src/main/java/com/nextcloud/android/sso/InputStreamBinder.java index 55edb300c..ac9e62bd1 100644 --- a/app/src/main/java/com/nextcloud/android/sso/InputStreamBinder.java +++ b/app/src/main/java/com/nextcloud/android/sso/InputStreamBinder.java @@ -32,7 +32,6 @@ import android.content.Context; import android.content.SharedPreferences; import android.net.Uri; import android.os.Binder; -import android.os.Build; import android.os.ParcelFileDescriptor; import android.text.TextUtils; @@ -74,10 +73,8 @@ import java.io.InputStreamReader; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; -import java.text.SimpleDateFormat; import java.util.Collection; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.logging.Level; @@ -387,9 +384,7 @@ public class InputStreamBinder extends IInputStreamService.Stub { } private static String getUserAgent() { - SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyMMdd", Locale.getDefault()); - String time = simpleDateFormat.format(Build.TIME); - return "eos(" + time + ")-AccountManager-SSO(" + BuildConfig.VERSION_NAME + ")"; + return "AccountManager-SSO(" + BuildConfig.VERSION_NAME + ")"; } private Response processRequestV2(final NextcloudRequest request, final InputStream requestBodyInputStream) -- GitLab From 1259f7dee309553477a0ef75123a8dc4c330ec53 Mon Sep 17 00:00:00 2001 From: Fahim Salam Chowdhury Date: Tue, 20 Feb 2024 17:24:19 +0600 Subject: [PATCH 11/12] chore: refactor according to review --- .../android/sso/InputStreamBinder.java | 12 ++++++------ .../davdroid/network/PersistentCookieStore.kt | 18 ++++++++---------- .../davdroid/syncadapter/AccountUtils.kt | 4 ++-- 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/nextcloud/android/sso/InputStreamBinder.java b/app/src/main/java/com/nextcloud/android/sso/InputStreamBinder.java index ac9e62bd1..219123eed 100644 --- a/app/src/main/java/com/nextcloud/android/sso/InputStreamBinder.java +++ b/app/src/main/java/com/nextcloud/android/sso/InputStreamBinder.java @@ -334,9 +334,9 @@ public class InputStreamBinder extends IInputStreamService.Stub { } OwnCloudClientManagerFactory.setUserAgent(getUserAgent()); - OwnCloudClientManager ownCloudClientManager = OwnCloudClientManagerFactory.getDefaultSingleton(); - OwnCloudAccount ocAccount = new OwnCloudAccount(account, context); - OwnCloudClient client = ownCloudClientManager.getClientFor(ocAccount, context); + final OwnCloudClientManager ownCloudClientManager = OwnCloudClientManagerFactory.getDefaultSingleton(); + final OwnCloudAccount ownCloudAccount = new OwnCloudAccount(account, context); + final OwnCloudClient client = ownCloudClientManager.getClientFor(ownCloudAccount, context); HttpMethodBase method = buildMethod(request, client.getBaseUri(), requestBodyInputStream); @@ -408,9 +408,9 @@ public class InputStreamBinder extends IInputStreamService.Stub { } OwnCloudClientManagerFactory.setUserAgent(getUserAgent()); - OwnCloudClientManager ownCloudClientManager = OwnCloudClientManagerFactory.getDefaultSingleton(); - OwnCloudAccount ocAccount = new OwnCloudAccount(account, context); - OwnCloudClient client = ownCloudClientManager.getClientFor(ocAccount, context); + final OwnCloudClientManager ownCloudClientManager = OwnCloudClientManagerFactory.getDefaultSingleton(); + final OwnCloudAccount ownCloudAccount = new OwnCloudAccount(account, context); + final OwnCloudClient client = ownCloudClientManager.getClientFor(ownCloudAccount, context); HttpMethodBase method = buildMethod(request, client.getBaseUri(), requestBodyInputStream); diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/PersistentCookieStore.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/PersistentCookieStore.kt index 6170ea2db..37eada6e8 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/network/PersistentCookieStore.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/network/PersistentCookieStore.kt @@ -56,20 +56,18 @@ class PersistentCookieStore(context: Context, private val account: Account): Coo private fun getCookieMap(url: HttpUrl): HashMap { val result = HashMap() + val cookiesString = accountManager.getUserData(account, AccountSettings.COOKIE_KEY)?: return HashMap() - val cookiesString = accountManager.getUserData(account, AccountSettings.COOKIE_KEY) - ?: return HashMap() + val cookies = cookiesString.split(AccountSettings.COOKIE_SEPARATOR.toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray() - val cookies = cookiesString.split(AccountSettings.COOKIE_SEPARATOR.toRegex()).dropLastWhile { it.isEmpty() } - .toTypedArray() + cookies.forEach { + val cookie = Cookie.parse(url, it) ?: return@forEach - cookies.forEach { - val cookie = Cookie.parse(url, it) ?: return@forEach - - if (cookie.expiresAt > System.currentTimeMillis()) { - result[cookie.name] = cookie - } + if (cookie.expiresAt > System.currentTimeMillis()) { + result[cookie.name] = cookie } + } return result } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt index d364f4188..d94c06b0c 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/AccountUtils.kt @@ -132,7 +132,7 @@ object AccountUtils { } fun getOwnCloudBaseUrl(baseURL: String): String { - if (baseURL.contains("remote.php")) { + if (baseURL.contains("/remote.php")) { return baseURL.split("/remote.php")[0] } @@ -141,7 +141,7 @@ object AccountUtils { fun getAccount(context: Context, userName: String?, requestedBaseUrl: String?): Account? { if (userName == null || requestedBaseUrl == null) { - return null; + return null } val baseUrl = getOwnCloudBaseUrl(requestedBaseUrl) -- GitLab From 2cc52015145f723cf4d135b1dace90c1ffe04405 Mon Sep 17 00:00:00 2001 From: Fahim Salam Chowdhury Date: Thu, 22 Feb 2024 16:45:57 +0600 Subject: [PATCH 12/12] chore: update according to review --- app/build.gradle | 1 + .../activity/SsoGrantPermissionActivity.java | 4 +- .../davdroid/settings/AccountSettings.kt | 4 +- .../util/{SSOUtils.kt => SsoUtils.kt} | 15 ++--- .../at/bitfire/davdroid/util/SsoUtilsTest.kt | 56 +++++++++++++++++++ 5 files changed, 69 insertions(+), 11 deletions(-) rename app/src/main/kotlin/at/bitfire/davdroid/util/{SSOUtils.kt => SsoUtils.kt} (82%) create mode 100644 app/src/test/kotlin/at/bitfire/davdroid/util/SsoUtilsTest.kt diff --git a/app/build.gradle b/app/build.gradle index bb3c67378..e082e17c1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -147,6 +147,7 @@ configurations { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:${versions.kotlin}" implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3' + testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' implementation "com.google.dagger:hilt-android:${versions.hilt}" diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SsoGrantPermissionActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/SsoGrantPermissionActivity.java index 4c61a25b5..d74e50312 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SsoGrantPermissionActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/SsoGrantPermissionActivity.java @@ -48,7 +48,7 @@ import java.util.logging.Level; import at.bitfire.davdroid.R; import at.bitfire.davdroid.log.Logger; -import at.bitfire.davdroid.util.SSOUtils; +import at.bitfire.davdroid.util.SsoUtils; public class SsoGrantPermissionActivity extends AppCompatActivity { @@ -115,7 +115,7 @@ public class SsoGrantPermissionActivity extends AppCompatActivity { // create token String token = UUID.randomUUID().toString().replaceAll("-", ""); - String userId = SSOUtils.INSTANCE.sanitizeUserId(account.name); + String userId = SsoUtils.INSTANCE.sanitizeUserId(account.name); saveToken(token, account.name); setResultData(token, userId, serverUrl); diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt index e9349e631..5cda84f5d 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt @@ -19,7 +19,7 @@ import at.bitfire.davdroid.resource.LocalAddressBook import at.bitfire.davdroid.syncadapter.AccountUtils import at.bitfire.davdroid.syncadapter.PeriodicSyncWorker import at.bitfire.davdroid.syncadapter.SyncUtils -import at.bitfire.davdroid.util.SSOUtils +import at.bitfire.davdroid.util.SsoUtils import at.bitfire.davdroid.util.setAndVerifyUserData import at.bitfire.ical4android.TaskProvider import at.bitfire.vcard4android.GroupMethod @@ -139,7 +139,7 @@ class AccountSettings( bundle.putString(KEY_EMAIL_ADDRESS, credentials.userName) } - val userId = SSOUtils.sanitizeUserId(credentials.userName) + val userId = SsoUtils.sanitizeUserId(credentials.userName) bundle.putString(NCAccountUtils.Constants.KEY_USER_ID, userId) } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/util/SSOUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/util/SsoUtils.kt similarity index 82% rename from app/src/main/kotlin/at/bitfire/davdroid/util/SSOUtils.kt rename to app/src/main/kotlin/at/bitfire/davdroid/util/SsoUtils.kt index 9ba2077da..8edee609e 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/util/SSOUtils.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/util/SsoUtils.kt @@ -16,17 +16,19 @@ package at.bitfire.davdroid.util -object SSOUtils { +object SsoUtils { + + private const val EELO_EMAIL_DOMAIN = "@e.email" + /** * Murena account's userId is set same as it's email address. * For old accounts (@e.email) userId = email. * For new accounts (@murena.io) & other NC accounts userId is first part of email (ex: for email abc@murena.io, userId is abc). * For api requests, we needed to pass the actual userId. This method remove the unwanted part (ex: @murena.io) from the userId */ - fun sanitizeUserId(userId: String): String { - val eeloMailEndPart = "@e.email" - - if (userId.endsWith(eeloMailEndPart)) { + fun sanitizeUserId(param: String): String { + val userId = param.trim() + if (userId.endsWith(EELO_EMAIL_DOMAIN, ignoreCase = true)) { return userId } @@ -34,7 +36,6 @@ object SSOUtils { return userId } - return userId.split("@").dropLastWhile { it.isEmpty() } - .toTypedArray()[0] + return userId.substringBefore("@") } } diff --git a/app/src/test/kotlin/at/bitfire/davdroid/util/SsoUtilsTest.kt b/app/src/test/kotlin/at/bitfire/davdroid/util/SsoUtilsTest.kt new file mode 100644 index 000000000..5a4f70cf5 --- /dev/null +++ b/app/src/test/kotlin/at/bitfire/davdroid/util/SsoUtilsTest.kt @@ -0,0 +1,56 @@ +/* + * Copyright MURENA SAS 2024 + * This program 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. + * + * This program 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 this program. If not, see . + */ + +package at.bitfire.davdroid.util + +import org.junit.jupiter.api.Assertions.* + +import org.junit.jupiter.api.Test + +class SsoUtilsTest { + + @Test + fun `test sanitizeUserId with empty input`() { + val userId = "" + val expected = "" + val actual = SsoUtils.sanitizeUserId(userId) + assertEquals(expected, actual) + } + + @Test + fun `test sanitizeUserId with input without '@'`() { + val userId = "username" + val expected = "username" + val actual = SsoUtils.sanitizeUserId(userId) + assertEquals(expected, actual) + } + + @Test + fun `test sanitizeUserId with case sensitivity`() { + val userId = "User@E.EMAIL" + val expected = "User@E.EMAIL" + val actual = SsoUtils.sanitizeUserId(userId) + assertEquals(expected, actual) + } + + @Test + fun `test sanitizeUserId with leading or trailing spaces`() { + val userId = " user@domain.com " + val expected = "user" + val actual = SsoUtils.sanitizeUserId(userId) + assertEquals(expected, actual) + } +} -- GitLab