v1.0.24: harmonious icon, recursive file watching, download-then-open, security fixes
Icon: three identical parallel arcing arrows (same bezier curve, same blue-to-teal gradient #64C8FF→#32EDBB, same arrowhead geometry) — visually cohesive and clearly visible against the near-black background. FileWatchService: FileObserver is now recursive — watchDirRecursive() creates an observer for each subdirectory at startup, and adds new watchers when CREATE events produce new directories. Fixes files added to subdirectories not being detected. FilesViewModel: openFile/shareFile now fall back to download-then-open when the file is absent locally. AccountRepository + ProviderFactory injected; downloads to context.cacheDir/syncflow_open/ with isDownloading state. Path traversal guard added (reject relativePath containing ".."). file_paths.xml gains cache-path entry. WebDavProvider: path-traversal guard in parsePropfind — skip any server-returned filename containing "..", "/" or "\". Replace android.util.Log with Timber so debug logs are stripped from release builds. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
package com.syncflow.data.providers.webdav
|
||||
|
||||
import android.util.Log
|
||||
import com.syncflow.data.providers.CloudProvider
|
||||
import timber.log.Timber
|
||||
import com.syncflow.domain.model.CloudAccount
|
||||
import com.syncflow.domain.model.RemoteFile
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -59,14 +59,13 @@ open class WebDavProvider(protected val account: CloudAccount) : CloudProvider {
|
||||
withContext(Dispatchers.IO) {
|
||||
val req = Request.Builder().url(baseUrl).method("PROPFIND", PROPFIND_BODY).header("Depth", "0").build()
|
||||
client.newCall(req).execute().use { resp ->
|
||||
Log.d("SyncFlow/WebDAV", "testConnection ${resp.code} url=$baseUrl")
|
||||
Timber.d("WebDAV testConnection %d url=%s", resp.code, baseUrl)
|
||||
if (!resp.isSuccessful && resp.code != 207) {
|
||||
val body = resp.body?.string()?.take(300) ?: ""
|
||||
throw Exception("HTTP ${resp.code} ${resp.message} — $body")
|
||||
throw Exception("HTTP ${resp.code} ${resp.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}.onFailure { e -> Log.e("SyncFlow/WebDAV", "testConnection failed: ${e::class.simpleName}: ${e.message}", e.cause) }
|
||||
}.onFailure { e -> Timber.e(e, "WebDAV testConnection failed") }
|
||||
|
||||
override suspend fun listFiles(remotePath: String): Result<List<RemoteFile>> = runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
@@ -192,9 +191,14 @@ open class WebDavProvider(protected val account: CloudAccount) : CloudProvider {
|
||||
"response" -> if (inResponse && href.isNotBlank()) {
|
||||
val rawName = href.trimEnd('/').substringAfterLast('/')
|
||||
val name = try { java.net.URLDecoder.decode(rawName, "UTF-8") } catch (_: Exception) { rawName }
|
||||
val relPath = "$parentPath/$name".replace("//", "/")
|
||||
results.add(RemoteFile(relPath, name, isCollection, contentLength, lastModified, etag, contentType))
|
||||
inResponse = false
|
||||
// Guard against path-traversal sequences delivered by a malicious server
|
||||
if (name.contains("..") || name.contains('/') || name.contains('\\')) {
|
||||
inResponse = false
|
||||
} else {
|
||||
val relPath = "$parentPath/$name".replace("//", "/")
|
||||
results.add(RemoteFile(relPath, name, isCollection, contentLength, lastModified, etag, contentType))
|
||||
inResponse = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import javax.inject.Singleton
|
||||
class CredentialStore @Inject constructor(@ApplicationContext private val context: Context) {
|
||||
|
||||
private val prefs: SharedPreferences by lazy {
|
||||
@Suppress("DEPRECATION")
|
||||
val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
|
||||
EncryptedSharedPreferences.create(
|
||||
"syncflow_credentials",
|
||||
|
||||
Reference in New Issue
Block a user