Add EteBaseClientProvider

pull/1244/head
Alex Baker 5 years ago
parent af2213d60f
commit 9470eb2786

@ -6,12 +6,18 @@ import com.etesync.journalmanager.UserInfoManager.UserInfo
import org.tasks.ui.CompletableViewModel import org.tasks.ui.CompletableViewModel
class AddEteBaseAccountViewModel @ViewModelInject constructor( class AddEteBaseAccountViewModel @ViewModelInject constructor(
private val client: EteBaseClient): CompletableViewModel<Pair<UserInfo, String>>() { private val clientProvider: EteBaseClientProvider): CompletableViewModel<Pair<UserInfo, String>>() {
suspend fun addAccount(url: String, username: String, password: String) { suspend fun addAccount(url: String, username: String, password: String) {
run { run {
client.setForeground() val token =
val token = client.forUrl(url, username, null, null).getToken(password) clientProvider
Pair.create(client.forUrl(url, username, null, token!!).userInfo(), token) .forUrl(url, username, null, null)
.setForeground()
.getToken(password)
Pair.create(
clientProvider.forUrl(url, username, null, token!!).userInfo(),
token
)
} }
} }
} }

@ -5,8 +5,8 @@ import org.tasks.data.CaldavAccount
import org.tasks.ui.CompletableViewModel import org.tasks.ui.CompletableViewModel
class CreateCalendarViewModel @ViewModelInject constructor( class CreateCalendarViewModel @ViewModelInject constructor(
private val client: EteBaseClient) : CompletableViewModel<String?>() { private val clientProvider: EteBaseClientProvider) : CompletableViewModel<String?>() {
suspend fun createCalendar(account: CaldavAccount, name: String, color: Int) { suspend fun createCalendar(account: CaldavAccount, name: String, color: Int) {
run { client.forAccount(account).makeCollection(name, color) } run { clientProvider.forAccount(account).makeCollection(name, color) }
} }
} }

@ -5,10 +5,10 @@ import org.tasks.data.CaldavAccount
import org.tasks.ui.CompletableViewModel import org.tasks.ui.CompletableViewModel
class CreateUserInfoViewModel @ViewModelInject constructor( class CreateUserInfoViewModel @ViewModelInject constructor(
private val client: EteBaseClient): CompletableViewModel<String>() { private val clientProvider: EteBaseClientProvider): CompletableViewModel<String>() {
suspend fun createUserInfo(caldavAccount: CaldavAccount, derivedKey: String) { suspend fun createUserInfo(caldavAccount: CaldavAccount, derivedKey: String) {
run { run {
client.forAccount(caldavAccount).createUserInfo(derivedKey) clientProvider.forAccount(caldavAccount).createUserInfo(derivedKey)
derivedKey derivedKey
} }
} }

@ -6,8 +6,8 @@ import org.tasks.data.CaldavCalendar
import org.tasks.ui.ActionViewModel import org.tasks.ui.ActionViewModel
class DeleteCalendarViewModel @ViewModelInject constructor( class DeleteCalendarViewModel @ViewModelInject constructor(
private val client: EteBaseClient) : ActionViewModel() { private val clientProvider: EteBaseClientProvider) : ActionViewModel() {
suspend fun deleteCalendar(account: CaldavAccount, calendar: CaldavCalendar) { suspend fun deleteCalendar(account: CaldavAccount, calendar: CaldavCalendar) {
run { client.forAccount(account).deleteCollection(calendar) } run { clientProvider.forAccount(account).deleteCollection(calendar) }
} }
} }

@ -27,7 +27,7 @@ import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class EteBaseAccountSettingsActivity : BaseCaldavAccountSettingsActivity(), Toolbar.OnMenuItemClickListener { class EteBaseAccountSettingsActivity : BaseCaldavAccountSettingsActivity(), Toolbar.OnMenuItemClickListener {
@Inject lateinit var eteBaseClient: EteBaseClient @Inject lateinit var clientProvider: EteBaseClientProvider
private val addAccountViewModel: AddEteBaseAccountViewModel by viewModels() private val addAccountViewModel: AddEteBaseAccountViewModel by viewModels()
private val updateAccountViewModel: UpdateEteBaseAccountViewModel by viewModels() private val updateAccountViewModel: UpdateEteBaseAccountViewModel by viewModels()
@ -173,7 +173,7 @@ class EteBaseAccountSettingsActivity : BaseCaldavAccountSettingsActivity(), Tool
} }
override suspend fun removeAccount() { override suspend fun removeAccount() {
caldavAccount?.let { eteBaseClient.forAccount(it).invalidateToken() } caldavAccount?.let { clientProvider.forAccount(it).invalidateToken() }
super.removeAccount() super.removeAccount()
} }

@ -1,6 +1,5 @@
package org.tasks.etebase package org.tasks.etebase
import android.content.Context
import androidx.core.util.Pair import androidx.core.util.Pair
import at.bitfire.cert4android.CustomCertManager import at.bitfire.cert4android.CustomCertManager
import com.etesync.journalmanager.* import com.etesync.journalmanager.*
@ -14,134 +13,34 @@ import com.etesync.journalmanager.UserInfoManager.UserInfo.Companion.generate
import com.etesync.journalmanager.model.CollectionInfo import com.etesync.journalmanager.model.CollectionInfo
import com.etesync.journalmanager.model.CollectionInfo.Companion.fromJson import com.etesync.journalmanager.model.CollectionInfo.Companion.fromJson
import com.etesync.journalmanager.model.SyncEntry import com.etesync.journalmanager.model.SyncEntry
import com.etesync.journalmanager.util.TokenAuthenticator
import com.google.common.collect.Lists import com.google.common.collect.Lists
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.internal.tls.OkHostnameVerifier
import org.tasks.DebugNetworkInterceptor
import org.tasks.caldav.MemoryCookieStore
import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavCalendar import org.tasks.data.CaldavCalendar
import org.tasks.preferences.Preferences
import org.tasks.security.KeyStoreEncryption
import timber.log.Timber import timber.log.Timber
import java.io.IOException import java.io.IOException
import java.security.KeyManagementException
import java.security.NoSuchAlgorithmException
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.net.ssl.SSLContext
class EteBaseClient { class EteBaseClient(
private val encryption: KeyStoreEncryption private val customCertManager: CustomCertManager,
private val preferences: Preferences private val username: String?,
private val interceptor: DebugNetworkInterceptor private val encryptionPassword: String?,
private val username: String? private val token: String?,
private val token: String? private val httpClient: OkHttpClient,
private val encryptionPassword: String? private val httpUrl: HttpUrl
private val httpClient: OkHttpClient? ) {
private val httpUrl: HttpUrl? private val journalManager = JournalManager(httpClient, httpUrl)
private val context: Context
private val journalManager: JournalManager?
private var foreground = false
@Inject
constructor(
@ApplicationContext context: Context,
encryption: KeyStoreEncryption,
preferences: Preferences,
interceptor: DebugNetworkInterceptor) {
this.context = context
this.encryption = encryption
this.preferences = preferences
this.interceptor = interceptor
username = null
token = null
encryptionPassword = null
httpClient = null
httpUrl = null
journalManager = null
}
private constructor(
context: Context,
encryption: KeyStoreEncryption,
preferences: Preferences,
interceptor: DebugNetworkInterceptor,
url: String?,
username: String?,
encryptionPassword: String?,
token: String?,
foreground: Boolean) {
this.context = context
this.encryption = encryption
this.preferences = preferences
this.interceptor = interceptor
this.username = username
this.encryptionPassword = encryptionPassword
this.token = token
this.foreground = foreground
val customCertManager = CustomCertManager(context)
customCertManager.appInForeground = foreground
val hostnameVerifier = customCertManager.hostnameVerifier(OkHostnameVerifier)
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(null, arrayOf(customCertManager), null)
val builder = OkHttpClient()
.newBuilder()
.addNetworkInterceptor(TokenAuthenticator(null, token))
.cookieJar(MemoryCookieStore())
.followRedirects(false)
.followSslRedirects(true)
.sslSocketFactory(sslContext.socketFactory, customCertManager)
.hostnameVerifier(hostnameVerifier)
.connectTimeout(15, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
if (preferences.isFlipperEnabled) {
interceptor.apply(builder)
}
httpClient = builder.build()
httpUrl = url?.toHttpUrlOrNull()
journalManager = JournalManager(httpClient, httpUrl!!)
}
@Throws(NoSuchAlgorithmException::class, KeyManagementException::class)
suspend fun forAccount(account: CaldavAccount): EteBaseClient {
return forUrl(
account.url,
account.username,
account.getEncryptionPassword(encryption),
account.getPassword(encryption))
}
@Throws(KeyManagementException::class, NoSuchAlgorithmException::class)
suspend fun forUrl(url: String?, username: String?, encryptionPassword: String?, token: String?): EteBaseClient = withContext(Dispatchers.IO) {
EteBaseClient(
context,
encryption,
preferences,
interceptor,
url,
username,
encryptionPassword,
token,
foreground)
}
@Throws(IOException::class, Exceptions.HttpException::class) @Throws(IOException::class, Exceptions.HttpException::class)
suspend fun getToken(password: String?): String? = withContext(Dispatchers.IO) { suspend fun getToken(password: String?): String? = withContext(Dispatchers.IO) {
JournalAuthenticator(httpClient!!, httpUrl!!).getAuthToken(username!!, password!!) JournalAuthenticator(httpClient, httpUrl).getAuthToken(username!!, password!!)
} }
@Throws(Exceptions.HttpException::class) @Throws(Exceptions.HttpException::class)
suspend fun userInfo(): UserInfoManager.UserInfo? = withContext(Dispatchers.IO) { suspend fun userInfo(): UserInfoManager.UserInfo? = withContext(Dispatchers.IO) {
val userInfoManager = UserInfoManager(httpClient!!, httpUrl!!) val userInfoManager = UserInfoManager(httpClient, httpUrl)
userInfoManager.fetch(username!!) userInfoManager.fetch(username!!)
} }
@ -177,7 +76,7 @@ class EteBaseClient {
@Throws(Exceptions.HttpException::class) @Throws(Exceptions.HttpException::class)
suspend fun getCalendars(userInfo: UserInfoManager.UserInfo?): Map<Journal, CollectionInfo> = withContext(Dispatchers.IO) { suspend fun getCalendars(userInfo: UserInfoManager.UserInfo?): Map<Journal, CollectionInfo> = withContext(Dispatchers.IO) {
val result: MutableMap<Journal, CollectionInfo> = HashMap() val result: MutableMap<Journal, CollectionInfo> = HashMap()
for (journal in journalManager!!.list()) { for (journal in journalManager.list()) {
val collection = convertJournalToCollection(userInfo, journal) val collection = convertJournalToCollection(userInfo, journal)
if (collection != null) { if (collection != null) {
if (TYPE_TASKS == collection.type) { if (TYPE_TASKS == collection.type) {
@ -197,7 +96,7 @@ class EteBaseClient {
journal: Journal, journal: Journal,
calendar: CaldavCalendar, calendar: CaldavCalendar,
callback: suspend (List<Pair<JournalEntryManager.Entry, SyncEntry>>) -> Unit) = withContext(Dispatchers.IO) { callback: suspend (List<Pair<JournalEntryManager.Entry, SyncEntry>>) -> Unit) = withContext(Dispatchers.IO) {
val journalEntryManager = JournalEntryManager(httpClient!!, httpUrl!!, journal.uid!!) val journalEntryManager = JournalEntryManager(httpClient, httpUrl, journal.uid!!)
val crypto = getCrypto(userInfo, journal) val crypto = getCrypto(userInfo, journal)
var journalEntries: List<JournalEntryManager.Entry> var journalEntries: List<JournalEntryManager.Entry>
do { do {
@ -211,20 +110,21 @@ class EteBaseClient {
@Throws(Exceptions.HttpException::class) @Throws(Exceptions.HttpException::class)
suspend fun pushEntries(journal: Journal, entries: List<JournalEntryManager.Entry>?, remoteCtag: String?) = withContext(Dispatchers.IO) { suspend fun pushEntries(journal: Journal, entries: List<JournalEntryManager.Entry>?, remoteCtag: String?) = withContext(Dispatchers.IO) {
var remoteCtag = remoteCtag var remoteCtag = remoteCtag
val journalEntryManager = JournalEntryManager(httpClient!!, httpUrl!!, journal.uid!!) val journalEntryManager = JournalEntryManager(httpClient, httpUrl, journal.uid!!)
for (partition in Lists.partition(entries!!, MAX_PUSH)) { for (partition in Lists.partition(entries!!, MAX_PUSH)) {
journalEntryManager.create(partition, remoteCtag) journalEntryManager.create(partition, remoteCtag)
remoteCtag = partition[partition.size - 1].uid remoteCtag = partition[partition.size - 1].uid
} }
} }
fun setForeground() { fun setForeground(): EteBaseClient {
foreground = true customCertManager.appInForeground = true
return this
} }
suspend fun invalidateToken() = withContext(Dispatchers.IO) { suspend fun invalidateToken() = withContext(Dispatchers.IO) {
try { try {
JournalAuthenticator(httpClient!!, httpUrl!!).invalidateAuthToken(token!!) JournalAuthenticator(httpClient, httpUrl).invalidateAuthToken(token!!)
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
} }
@ -240,14 +140,14 @@ class EteBaseClient {
collectionInfo.selected = true collectionInfo.selected = true
collectionInfo.color = if (color == 0) null else color collectionInfo.color = if (color == 0) null else color
val crypto = CryptoManager(collectionInfo.version, encryptionPassword!!, uid) val crypto = CryptoManager(collectionInfo.version, encryptionPassword!!, uid)
journalManager!!.create(Journal(crypto, collectionInfo.toJson(), uid)) journalManager.create(Journal(crypto, collectionInfo.toJson(), uid))
uid uid
} }
@Throws(VersionTooNewException::class, IntegrityException::class, Exceptions.HttpException::class) @Throws(VersionTooNewException::class, IntegrityException::class, Exceptions.HttpException::class)
suspend fun updateCollection(calendar: CaldavCalendar, name: String?, color: Int): String? = withContext(Dispatchers.IO) { suspend fun updateCollection(calendar: CaldavCalendar, name: String?, color: Int): String = withContext(Dispatchers.IO) {
val uid = calendar.url val uid = calendar.url
val journal = journalManager!!.fetch(uid!!) val journal = journalManager.fetch(uid!!)
val userInfo = userInfo() val userInfo = userInfo()
val crypto = getCrypto(userInfo, journal) val crypto = getCrypto(userInfo, journal)
val collectionInfo = convertJournalToCollection(userInfo, journal) val collectionInfo = convertJournalToCollection(userInfo, journal)
@ -259,14 +159,14 @@ class EteBaseClient {
@Throws(Exceptions.HttpException::class) @Throws(Exceptions.HttpException::class)
suspend fun deleteCollection(calendar: CaldavCalendar) = withContext(Dispatchers.IO) { suspend fun deleteCollection(calendar: CaldavCalendar) = withContext(Dispatchers.IO) {
journalManager!!.delete(Journal.fakeWithUid(calendar.url!!)) journalManager.delete(Journal.fakeWithUid(calendar.url!!))
} }
@Throws(Exceptions.HttpException::class, VersionTooNewException::class, IntegrityException::class, IOException::class) @Throws(Exceptions.HttpException::class, VersionTooNewException::class, IntegrityException::class, IOException::class)
suspend fun createUserInfo(derivedKey: String?) = withContext(Dispatchers.IO) { suspend fun createUserInfo(derivedKey: String?) = withContext(Dispatchers.IO) {
val cryptoManager = CryptoManager(CURRENT_VERSION, derivedKey!!, "userInfo") val cryptoManager = CryptoManager(CURRENT_VERSION, derivedKey!!, "userInfo")
val userInfo: UserInfoManager.UserInfo = generate(cryptoManager, username!!) val userInfo: UserInfoManager.UserInfo = generate(cryptoManager, username!!)
UserInfoManager(httpClient!!, httpUrl!!).create(userInfo) UserInfoManager(httpClient, httpUrl).create(userInfo)
} }
companion object { companion object {

@ -0,0 +1,80 @@
package org.tasks.etebase
import android.content.Context
import at.bitfire.cert4android.CustomCertManager
import com.etesync.journalmanager.util.TokenAuthenticator
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.internal.tls.OkHostnameVerifier
import org.tasks.DebugNetworkInterceptor
import org.tasks.caldav.MemoryCookieStore
import org.tasks.data.CaldavAccount
import org.tasks.preferences.Preferences
import org.tasks.security.KeyStoreEncryption
import java.security.KeyManagementException
import java.security.NoSuchAlgorithmException
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.net.ssl.SSLContext
class EteBaseClientProvider @Inject constructor(
@ApplicationContext private val context: Context,
private val encryption: KeyStoreEncryption,
private val preferences: Preferences,
private val interceptor: DebugNetworkInterceptor
) {
@Throws(NoSuchAlgorithmException::class, KeyManagementException::class)
suspend fun forAccount(account: CaldavAccount): EteBaseClient {
return forUrl(
account.url!!,
account.username,
account.getEncryptionPassword(encryption),
account.getPassword(encryption))
}
@Throws(KeyManagementException::class, NoSuchAlgorithmException::class)
suspend fun forUrl(url: String, username: String?, encryptionPassword: String?, token: String?): EteBaseClient = withContext(Dispatchers.IO) {
val customCertManager = newCertManager()
EteBaseClient(
customCertManager,
username,
encryptionPassword,
token,
createHttpClient(token, customCertManager),
url.toHttpUrl()
)
}
private suspend fun newCertManager() = withContext(Dispatchers.Default) {
CustomCertManager(context)
}
private fun createHttpClient(
token: String?,
customCertManager: CustomCertManager,
foreground: Boolean = false
): OkHttpClient {
customCertManager.appInForeground = foreground
val hostnameVerifier = customCertManager.hostnameVerifier(OkHostnameVerifier)
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(null, arrayOf(customCertManager), null)
val builder = OkHttpClient()
.newBuilder()
.addNetworkInterceptor(TokenAuthenticator(null, token))
.cookieJar(MemoryCookieStore())
.followRedirects(false)
.followSslRedirects(true)
.sslSocketFactory(sslContext.socketFactory, customCertManager)
.hostnameVerifier(hostnameVerifier)
.connectTimeout(15, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
if (preferences.isFlipperEnabled) {
interceptor.apply(builder)
}
return builder.build()
}
}

@ -39,7 +39,7 @@ class EteBaseSynchronizer @Inject constructor(
private val localBroadcastManager: LocalBroadcastManager, private val localBroadcastManager: LocalBroadcastManager,
private val taskDeleter: TaskDeleter, private val taskDeleter: TaskDeleter,
private val inventory: Inventory, private val inventory: Inventory,
private val client: EteBaseClient, private val clientProvider: EteBaseClientProvider,
private val iCal: iCalendar) { private val iCal: iCalendar) {
companion object { companion object {
init { init {
@ -79,7 +79,7 @@ class EteBaseSynchronizer @Inject constructor(
@Throws(KeyManagementException::class, NoSuchAlgorithmException::class, Exceptions.HttpException::class, IntegrityException::class, VersionTooNewException::class) @Throws(KeyManagementException::class, NoSuchAlgorithmException::class, Exceptions.HttpException::class, IntegrityException::class, VersionTooNewException::class)
private suspend fun synchronize(account: CaldavAccount) { private suspend fun synchronize(account: CaldavAccount) {
val client = client.forAccount(account) val client = clientProvider.forAccount(account)
val userInfo = client.userInfo() val userInfo = client.userInfo()
val resources = client.getCalendars(userInfo) val resources = client.getCalendars(userInfo)
val uids: Set<String> = resources.values.mapNotNull { it.uid }.toHashSet() val uids: Set<String> = resources.values.mapNotNull { it.uid }.toHashSet()

@ -6,8 +6,8 @@ import org.tasks.data.CaldavCalendar
import org.tasks.ui.CompletableViewModel import org.tasks.ui.CompletableViewModel
class UpdateCalendarViewModel @ViewModelInject constructor( class UpdateCalendarViewModel @ViewModelInject constructor(
private val client: EteBaseClient): CompletableViewModel<String?>() { private val clientProvider: EteBaseClientProvider): CompletableViewModel<String?>() {
suspend fun updateCalendar(account: CaldavAccount, calendar: CaldavCalendar, name: String, color: Int) { suspend fun updateCalendar(account: CaldavAccount, calendar: CaldavCalendar, name: String, color: Int) {
run { client.forAccount(account).updateCollection(calendar, name, color) } run { clientProvider.forAccount(account).updateCollection(calendar, name, color) }
} }
} }

@ -7,15 +7,24 @@ import org.tasks.Strings.isNullOrEmpty
import org.tasks.ui.CompletableViewModel import org.tasks.ui.CompletableViewModel
class UpdateEteBaseAccountViewModel @ViewModelInject constructor( class UpdateEteBaseAccountViewModel @ViewModelInject constructor(
private val client: EteBaseClient) : CompletableViewModel<Pair<UserInfo, String>>() { private val clientProvider: EteBaseClientProvider) : CompletableViewModel<Pair<UserInfo, String>>() {
suspend fun updateAccount(url: String, user: String, pass: String?, token: String) { suspend fun updateAccount(url: String, user: String, pass: String?, token: String) {
run { run {
client.setForeground()
if (isNullOrEmpty(pass)) { if (isNullOrEmpty(pass)) {
Pair.create(client.forUrl(url, user, null, token).userInfo(), token) Pair.create(
clientProvider.forUrl(url, user, null, token).setForeground().userInfo(),
token
)
} else { } else {
val newToken = client.forUrl(url, user, null, null).getToken(pass) val newToken =
Pair.create(client.forUrl(url, user, null, newToken).userInfo(), newToken) clientProvider
.forUrl(url, user, null, null)
.setForeground()
.getToken(pass)!!
Pair.create(
clientProvider.forUrl(url, user, null, newToken).userInfo(),
newToken
)
} }
} }
} }

Loading…
Cancel
Save