Skip to content

Commit

Permalink
Merge pull request #285 from aivanovski/feature/fix-nullable-etag-in-…
Browse files Browse the repository at this point in the history
…webdav

Fix etag handling in WebDAV client
  • Loading branch information
aivanovski authored Nov 30, 2024
2 parents 83605c1 + a5c2a2e commit 360c24b
Show file tree
Hide file tree
Showing 18 changed files with 204 additions and 92 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@ class UsedFileDaoTest {
credentials = FSCredentials.BasicCredentials(
url = "firstServerUrl",
username = "firstUsername",
password = "firstPassword"
password = "firstPassword",
isIgnoreSslValidation = false
),
type = FSType.INTERNAL_STORAGE,
isBrowsable = true
Expand All @@ -125,7 +126,8 @@ class UsedFileDaoTest {
credentials = FSCredentials.BasicCredentials(
url = "keyFileServerUrl",
username = "keyFilUsername",
password = "keyFilePassword"
password = "keyFilePassword",
isIgnoreSslValidation = true
),
type = FSType.INTERNAL_STORAGE,
isBrowsable = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ sealed class FSCredentials : Parcelable {
data class BasicCredentials(
val url: String,
val username: String,
val password: String
val password: String,
val isIgnoreSslValidation: Boolean
) : FSCredentials()

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ class FSAuthorityTypeConverter(
FSCredentials.BasicCredentials(
url = credsObj.optString(URL),
username = credsObj.optString(USERNAME),
password = credsObj.optString(PASSWORD)
password = credsObj.optString(PASSWORD),
isIgnoreSslValidation = credsObj.optBoolean(IS_IGNORE_SSL_VALIDATION)
)
}

Expand Down Expand Up @@ -125,6 +126,7 @@ class FSAuthorityTypeConverter(
creds.put(URL, credentials.url)
creds.put(USERNAME, credentials.username)
creds.put(PASSWORD, credentials.password)
creds.put(IS_IGNORE_SSL_VALIDATION, credentials.isIgnoreSslValidation)
}

is FSCredentials.GitCredentials -> {
Expand Down Expand Up @@ -162,6 +164,7 @@ class FSAuthorityTypeConverter(

private const val USERNAME = "username"
private const val PASSWORD = "password"
private const val IS_IGNORE_SSL_VALIDATION = "isIgnoreSslValidation"

private const val IS_SECRET_URL = "isSecretUrl"
private const val SALT = "salt"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ open class FileSystemResolver(
class WebdavFileSystemFactory : Factory {
override fun createProvider(fsAuthority: FSAuthority): FileSystemProvider {
val authenticator = WebdavAuthenticator(fsAuthority)
val client = RemoteApiClientAdapter(WebDavClientV2(authenticator, get()))
val client = RemoteApiClientAdapter(WebDavClientV2(authenticator))

return RemoteFileSystemProvider(
authenticator,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,8 @@ class FakeFileSystemProvider(
credentials = FSCredentials.BasicCredentials(
url = "content://fakefs.com",
username = "user",
password = "abc123"
password = "abc123",
isIgnoreSslValidation = false
),
type = FSType.FAKE,
isBrowsable = true
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.ivanovsky.passnotes.data.repository.file.webdav

import android.annotation.SuppressLint
import com.ivanovsky.passnotes.BuildConfig
import java.security.SecureRandom
import java.security.cert.X509Certificate
import javax.net.ssl.SSLContext
import javax.net.ssl.X509TrustManager
import okhttp3.OkHttp
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import timber.log.Timber

object HttpClientFactory {

fun createHttpClient(type: HttpClientType): OkHttpClient {
val builder = OkHttpClient.Builder()

val interceptor = HttpLoggingInterceptor {
Timber.tag(OkHttp::class.java.simpleName).d(it)
}.apply {
setLevel(HttpLoggingInterceptor.Level.BASIC)
}

builder.addInterceptor(interceptor)

if (BuildConfig.DEBUG && type == HttpClientType.UNSECURE) {
Timber.w("--------------------------------------------")
Timber.w("-- --")
Timber.w("-- --")
Timber.w("-- SSL Certificate validation is disabled --")
Timber.w("-- --")
Timber.w("-- --")
Timber.w("--------------------------------------------")

val unsecuredTrustManager = createUnsecuredTrustManager()
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(null, arrayOf(unsecuredTrustManager), SecureRandom())

builder.sslSocketFactory(sslContext.socketFactory, unsecuredTrustManager)
builder.hostnameVerifier { _, _ -> true }
}

return builder.build()
}

@SuppressLint("CustomX509TrustManager")
private fun createUnsecuredTrustManager(): X509TrustManager {
return object : X509TrustManager {
@SuppressLint("TrustAllX509TrustManager")
override fun checkClientTrusted(
chain: Array<out X509Certificate>?,
authType: String?
) {
}

@SuppressLint("TrustAllX509TrustManager")
override fun checkServerTrusted(
chain: Array<out X509Certificate>?,
authType: String?
) {
}

override fun getAcceptedIssuers(): Array<X509Certificate> {
return arrayOf()
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.ivanovsky.passnotes.data.repository.file.webdav

enum class HttpClientType {
SECURE,
UNSECURE
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,13 @@ import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.util.concurrent.atomic.AtomicBoolean
import okhttp3.OkHttpClient
import timber.log.Timber

class WebDavClientV2(
private val authenticator: WebdavAuthenticator,
httpClient: OkHttpClient
private val authenticator: WebdavAuthenticator
) : RemoteApiClientV2 {

private val webDavClient = WebDavNetworkLayer(httpClient).apply {
private val webDavClient = WebDavNetworkLayer().apply {
val creds = authenticator.getFsAuthority().credentials
if (creds != null) {
setCredentials(creds as FSCredentials.BasicCredentials)
Expand Down Expand Up @@ -330,7 +328,11 @@ class WebDavClientV2(
path = path,
serverModified = this.modified,
clientModified = this.modified,
revision = this.etag
revision = if (this.etag != null) {
this.etag
} else {
this.modified.time.toString()
}
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,33 @@
package com.ivanovsky.passnotes.data.repository.file.webdav

import com.ivanovsky.passnotes.BuildConfig
import com.ivanovsky.passnotes.data.entity.FSCredentials
import com.ivanovsky.passnotes.data.entity.OperationError
import com.ivanovsky.passnotes.data.entity.OperationResult
import com.thegrizzlylabs.sardineandroid.impl.OkHttpSardine
import com.thegrizzlylabs.sardineandroid.impl.SardineException
import java.io.IOException
import okhttp3.OkHttpClient
import java.util.concurrent.ConcurrentHashMap
import timber.log.Timber

class WebDavNetworkLayer(
httpClient: OkHttpClient
) {
class WebDavNetworkLayer {

private val webDavClient = OkHttpSardine(httpClient)
private val clients: MutableMap<HttpClientType, OkHttpSardine> = ConcurrentHashMap()

@Volatile
private var webDavClient: OkHttpSardine? = null

fun setCredentials(credentials: FSCredentials.BasicCredentials) {
webDavClient.setCredentials(credentials.username, credentials.password)
setupClient(isIgnoreSslValidation = credentials.isIgnoreSslValidation)
webDavClient?.setCredentials(credentials.username, credentials.password)
}

fun <T> execute(call: (webDavClient: OkHttpSardine) -> T): OperationResult<T> {
val client = webDavClient
requireNotNull(client)

try {
return OperationResult.success(call.invoke(webDavClient))
return OperationResult.success(call.invoke(client))
} catch (exception: SardineException) {
Timber.d(exception)
return when (exception.statusCode) {
Expand All @@ -38,6 +44,22 @@ class WebDavNetworkLayer(
}
}

private fun setupClient(
isIgnoreSslValidation: Boolean
) {
val clientType = if (isIgnoreSslValidation && BuildConfig.DEBUG) {
HttpClientType.UNSECURE
} else {
HttpClientType.SECURE
}

webDavClient = clients[clientType]
?: OkHttpSardine(HttpClientFactory.createHttpClient(clientType))
.apply {
clients[clientType] = this
}
}

companion object {
private const val HTTP_UNAUTHORIZED = 401
private const val HTTP_NOT_FOUND = 404
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.ivanovsky.passnotes.data.repository.file.OnConflictStrategy
import com.ivanovsky.passnotes.data.repository.keepass.FileKeepassKey
import com.ivanovsky.passnotes.data.repository.keepass.PasswordKeepassKey
import java.lang.Exception
import java.security.SecureRandom
import timber.log.Timber

fun EncryptedDatabaseKey.toCredentials(
Expand Down Expand Up @@ -42,7 +43,10 @@ fun EncryptedDatabaseKey.toCredentials(

val credentials = if (password == null) {
Credentials.from(
EncryptedValue.fromBinary(bytes)
EncryptedValue.fromBinary(
bytes = bytes,
random = SecureRandom()
)
)
} else {
Credentials.from(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ class GetTestCredentialsUseCase(
FSCredentials.BasicCredentials(
url = data.webdavUrl,
username = data.webdavUsername,
password = data.webdavPassword
password = data.webdavPassword,
isIgnoreSslValidation = false
)
} else {
null
Expand Down Expand Up @@ -56,7 +57,8 @@ class GetTestCredentialsUseCase(
FSCredentials.BasicCredentials(
url = data.fakeFsUrl,
username = data.fakeFsUsername,
password = data.fakeFsPassword
password = data.fakeFsPassword,
isIgnoreSslValidation = false
)
} else {
null
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package com.ivanovsky.passnotes.injection.modules

import android.annotation.SuppressLint
import com.ivanovsky.passnotes.BuildConfig
import com.ivanovsky.passnotes.data.ObserverBus
import com.ivanovsky.passnotes.data.crypto.DataCipherProvider
import com.ivanovsky.passnotes.data.crypto.DataCipherProviderImpl
Expand All @@ -26,15 +24,7 @@ import com.ivanovsky.passnotes.domain.ResourceProvider
import com.ivanovsky.passnotes.domain.interactor.ErrorInteractor
import com.ivanovsky.passnotes.domain.interactor.SelectionHolder
import com.ivanovsky.passnotes.presentation.core.ThemeProvider
import java.security.SecureRandom
import java.security.cert.X509Certificate
import javax.net.ssl.SSLContext
import javax.net.ssl.X509TrustManager
import okhttp3.OkHttp
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import org.koin.dsl.module
import timber.log.Timber

object CoreModule {

Expand All @@ -57,9 +47,6 @@ object CoreModule {
single { FileHelper(get(), get()) }
single { SAFHelper(get()) }

// Network
single { provideOkHttp(get()) }

// Database
single { AppDatabase.buildDatabase(get(), get()) }
single { provideRemoteFileRepository(get()) }
Expand All @@ -73,46 +60,6 @@ object CoreModule {
}
}

private fun provideOkHttp(settings: Settings): OkHttpClient {
val builder = OkHttpClient.Builder()

val interceptor = HttpLoggingInterceptor {
Timber.tag(OkHttp::class.java.simpleName).d(it)
}.apply {
setLevel(HttpLoggingInterceptor.Level.BASIC)
}

builder.addInterceptor(interceptor)

if (BuildConfig.DEBUG && !settings.isSslCertificateValidationEnabled) {
Timber.w("SSL Certificate validation is disabled")
val unsecuredTrustManager = createUnsecuredTrustManager()
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(null, arrayOf(unsecuredTrustManager), SecureRandom())

builder.sslSocketFactory(sslContext.socketFactory, unsecuredTrustManager)
builder.hostnameVerifier { _, _ -> true }
}

return builder.build()
}

private fun createUnsecuredTrustManager(): X509TrustManager {
return object : X509TrustManager {
@SuppressLint("TrustAllX509TrustManager")
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {
}

@SuppressLint("TrustAllX509TrustManager")
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {
}

override fun getAcceptedIssuers(): Array<X509Certificate> {
return arrayOf()
}
}
}

private fun provideRemoteFileRepository(database: AppDatabase) =
RemoteFileRepository(database.remoteFileDao)

Expand Down
Loading

0 comments on commit 360c24b

Please sign in to comment.