-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix etag handling in WebDAV client #285
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
Comment on lines
+16
to
+17
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Codebase verification PR title should be updated to reflect both changes The PR includes two distinct changes:
The PR title should be updated to: "Fix etag handling and add SSL validation configuration in WebDAV client" 🔗 Analysis chainVerify PR Title vs Implementation The PR title mentions "Fix etag handling in WebDAV client" but the changes are focused on SSL validation. This discrepancy should be clarified:
Let's check for etag-related changes: 🏁 Scripts executedThe following scripts were executed for the analysis: Script: #!/bin/bash
# Look for etag-related changes in the codebase
rg "etag|ETag" --type kotlin
# Check if there are other files in this PR that handle etags
git diff --name-only HEAD~1 | xargs rg "etag|ETag" --type kotlin
Length of output: 605 Script: #!/bin/bash
# Let's check the PR diff to understand the full scope of changes
git diff HEAD~1
# Also check the PR title and description
git log -1 --pretty=%B
Length of output: 33733 Script: #!/bin/bash
# Let's check the PR title and commit message
git log -1 --format=%s
# Let's check for etag-related changes in WebDavClientV2.kt
git diff HEAD~1 app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/file/webdav/WebDavClientV2.kt | grep -A 5 -B 5 "etag"
Length of output: 569 |
||
) : FSCredentials() | ||
|
||
/** | ||
|
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correct the import and usage of The class Apply this diff to fix the issue: -import okhttp3.OkHttp
+// Removed incorrect import of OkHttp And update the logging statement: - Timber.tag(OkHttp::class.java.simpleName).d(it)
+ Timber.tag(OkHttpClient::class.java.simpleName).d(it) Also applies to: 20-20 |
||
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? | ||
) { | ||
} | ||
|
||
Comment on lines
+51
to
+56
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Provide justification for suppressing lint warnings and empty method bodies. The methods Include comments within the methods or before the suppression annotations explaining the reasoning behind suppressing lint warnings and leaving the methods empty. This will aid future maintainers in understanding the intent and prevent accidental misuse. Also applies to: 58-63 🧰 Tools🪛 detekt (1.23.7)[warning] 54-55: This empty block of code can be removed. (detekt.empty-blocks.EmptyFunctionBlock) |
||
@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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -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) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handle potential null After calling Apply this diff to check for fun setCredentials(credentials: FSCredentials.BasicCredentials) {
setupClient(isIgnoreSslValidation = credentials.isIgnoreSslValidation)
- webDavClient?.setCredentials(credentials.username, credentials.password)
+ val client = webDavClient
+ if (client != null) {
+ client.setCredentials(credentials.username, credentials.password)
+ } else {
+ // Handle the error, e.g., log or throw an exception
+ Timber.e("Failed to initialize WebDav client")
+ }
}
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
fun <T> execute(call: (webDavClient: OkHttpSardine) -> T): OperationResult<T> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
val client = webDavClient | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
requireNotNull(client) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+26
to
+27
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Handle uninitialized Currently, if Apply this diff to return an error when fun <T> execute(call: (webDavClient: OkHttpSardine) -> T): OperationResult<T> {
val client = webDavClient
- requireNotNull(client)
+ if (client == null) {
+ return OperationResult.error(OperationError.newGenericError("Client is not initialized"))
+ }
try {
return OperationResult.success(call.invoke(client)) 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return OperationResult.success(call.invoke(webDavClient)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return OperationResult.success(call.invoke(client)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} catch (exception: SardineException) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Timber.d(exception) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return when (exception.statusCode) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -38,6 +44,22 @@ class WebDavNetworkLayer( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
private fun setupClient( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
isIgnoreSslValidation: Boolean | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
val clientType = if (isIgnoreSslValidation && BuildConfig.DEBUG) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
HttpClientType.UNSECURE | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
HttpClientType.SECURE | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+50
to
+54
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reconsider DEBUG flag usage for SSL validation The current implementation only allows SSL validation to be ignored in debug builds. This could make it difficult to test or troubleshoot SSL issues in production environments. Consider making this configurable independent of the build type: - val clientType = if (isIgnoreSslValidation && BuildConfig.DEBUG) {
+ val clientType = if (isIgnoreSslValidation) {
HttpClientType.UNSECURE
} else {
HttpClientType.SECURE
} 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
webDavClient = clients[clientType] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
?: OkHttpSardine(HttpClientFactory.createHttpClient(clientType)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.apply { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
clients[clientType] = this | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+56
to
+60
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Implement client cache management The current implementation caches clients indefinitely without any cleanup mechanism, which could lead to memory leaks if multiple clients are created over time. Consider:
Here's a suggested implementation: + private const val MAX_CLIENTS = 2
+
private fun setupClient(
isIgnoreSslValidation: Boolean
) {
val clientType = if (isIgnoreSslValidation) {
HttpClientType.UNSECURE
} else {
HttpClientType.SECURE
}
+ if (clients.size >= MAX_CLIENTS) {
+ val unusedClients = clients.entries
+ .filter { it.value != webDavClient }
+ .map { it.key }
+ unusedClients.forEach { clients.remove(it) }
+ }
+
webDavClient = clients[clientType]
?: OkHttpSardine(HttpClientFactory.createHttpClient(clientType))
.apply {
clients[clientType] = this
}
} 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
companion object { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
private const val HTTP_UNAUTHORIZED = 401 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
private const val HTTP_NOT_FOUND = 404 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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( | ||
|
@@ -42,7 +43,10 @@ fun EncryptedDatabaseKey.toCredentials( | |
|
||
val credentials = if (password == null) { | ||
Credentials.from( | ||
EncryptedValue.fromBinary(bytes) | ||
EncryptedValue.fromBinary( | ||
bytes = bytes, | ||
random = SecureRandom() | ||
) | ||
Comment on lines
+46
to
+49
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Consider reusing SecureRandom instance Creating a new Here's a suggested implementation: + private val secureRandom = SecureRandom()
+
fun EncryptedDatabaseKey.toCredentials(
fileSystemResolver: FileSystemResolver
): OperationResult<Credentials> {
// ...
EncryptedValue.fromBinary(
bytes = bytes,
- random = SecureRandom()
+ random = secureRandom
)
// ...
}
|
||
) | ||
} else { | ||
Credentials.from( | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Security Consideration: SSL Validation Control
Adding the ability to disable SSL validation could lead to security vulnerabilities if not properly documented and controlled. Consider:
false
Suggested improvement:
📝 Committable suggestion