Add content scanner service (#4392)
* Add content scanner APIs * Move to content scanner matrix SDK to FOSS * Update file service * Refactoring * Replace matrix callbacks by coroutines * Fix lint errors * Add changelog Co-authored-by: yostyle <yoanp@element.io>
This commit is contained in:
commit
855b672f48
2
changelog.d/4392.removal
Normal file
2
changelog.d/4392.removal
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Add content scanner API from MSC1453
|
||||||
|
API documentation : https://github.com/matrix-org/matrix-content-scanner#api
|
@ -18,6 +18,8 @@ package org.matrix.android.sdk.api.failure
|
|||||||
|
|
||||||
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
|
import org.matrix.android.sdk.api.session.contentscanner.ContentScannerError
|
||||||
|
import org.matrix.android.sdk.api.session.contentscanner.ScanFailure
|
||||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import javax.net.ssl.HttpsURLConnection
|
import javax.net.ssl.HttpsURLConnection
|
||||||
@ -100,3 +102,19 @@ fun Throwable.isRegistrationAvailabilityError(): Boolean {
|
|||||||
error.code == MatrixError.M_INVALID_USERNAME ||
|
error.code == MatrixError.M_INVALID_USERNAME ||
|
||||||
error.code == MatrixError.M_EXCLUSIVE)
|
error.code == MatrixError.M_EXCLUSIVE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to convert to a ScanFailure. Return null in the cases it's not possible
|
||||||
|
*/
|
||||||
|
fun Throwable.toScanFailure(): ScanFailure? {
|
||||||
|
return if (this is Failure.OtherServerError) {
|
||||||
|
tryOrNull {
|
||||||
|
MoshiProvider.providesMoshi()
|
||||||
|
.adapter(ContentScannerError::class.java)
|
||||||
|
.fromJson(errorBody)
|
||||||
|
}
|
||||||
|
?.let { ScanFailure(it, httpCode, this) }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -31,6 +31,7 @@ import org.matrix.android.sdk.api.session.cache.CacheService
|
|||||||
import org.matrix.android.sdk.api.session.call.CallSignalingService
|
import org.matrix.android.sdk.api.session.call.CallSignalingService
|
||||||
import org.matrix.android.sdk.api.session.content.ContentUploadStateTracker
|
import org.matrix.android.sdk.api.session.content.ContentUploadStateTracker
|
||||||
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
|
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
|
||||||
|
import org.matrix.android.sdk.api.session.contentscanner.ContentScannerService
|
||||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
||||||
import org.matrix.android.sdk.api.session.events.EventService
|
import org.matrix.android.sdk.api.session.events.EventService
|
||||||
import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker
|
import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker
|
||||||
@ -192,6 +193,11 @@ interface Session :
|
|||||||
*/
|
*/
|
||||||
fun cryptoService(): CryptoService
|
fun cryptoService(): CryptoService
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the ContentScannerService associated with the session
|
||||||
|
*/
|
||||||
|
fun contentScannerService(): ContentScannerService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the identity service associated with the session
|
* Returns the identity service associated with the session
|
||||||
*/
|
*/
|
||||||
|
@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
package org.matrix.android.sdk.api.session.content
|
package org.matrix.android.sdk.api.session.content
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This interface defines methods for accessing content from the current session.
|
* This interface defines methods for accessing content from the current session.
|
||||||
*/
|
*/
|
||||||
@ -39,6 +41,15 @@ interface ContentUrlResolver {
|
|||||||
*/
|
*/
|
||||||
fun resolveFullSize(contentUrl: String?): String?
|
fun resolveFullSize(contentUrl: String?): String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the ResolvedMethod to download a URL
|
||||||
|
*
|
||||||
|
* @param contentUrl the Matrix media content URI (in the form of "mxc://...").
|
||||||
|
* @param elementToDecrypt Encryption data may be required if you use a content scanner
|
||||||
|
* @return the Method to access resource, or null if invalid
|
||||||
|
*/
|
||||||
|
fun resolveForDownload(contentUrl: String?, elementToDecrypt: ElementToDecrypt? = null): ResolvedMethod?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the actual URL for accessing the thumbnail image of a given Matrix media content URI.
|
* Get the actual URL for accessing the thumbnail image of a given Matrix media content URI.
|
||||||
*
|
*
|
||||||
@ -49,4 +60,9 @@ interface ContentUrlResolver {
|
|||||||
* @return the URL to access the described resource, or null if the url is invalid.
|
* @return the URL to access the described resource, or null if the url is invalid.
|
||||||
*/
|
*/
|
||||||
fun resolveThumbnail(contentUrl: String?, width: Int, height: Int, method: ThumbnailMethod): String?
|
fun resolveThumbnail(contentUrl: String?, width: Int, height: Int, method: ThumbnailMethod): String?
|
||||||
|
|
||||||
|
sealed class ResolvedMethod {
|
||||||
|
data class GET(val url: String) : ResolvedMethod()
|
||||||
|
data class POST(val url: String, val jsonBody: String) : ResolvedMethod()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.api.session.contentscanner
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class ContentScannerError(
|
||||||
|
@Json(name = "info") val info: String? = null,
|
||||||
|
@Json(name = "reason") val reason: String? = null
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
// 502 The server failed to request media from the media repo.
|
||||||
|
const val REASON_MCS_MEDIA_REQUEST_FAILED = "MCS_MEDIA_REQUEST_FAILED"
|
||||||
|
|
||||||
|
/* 400 The server failed to decrypt the encrypted media downloaded from the media repo.*/
|
||||||
|
const val REASON_MCS_MEDIA_FAILED_TO_DECRYPT = "MCS_MEDIA_FAILED_TO_DECRYPT"
|
||||||
|
|
||||||
|
/* 403 The server scanned the downloaded media but the antivirus script returned a non-zero exit code.*/
|
||||||
|
const val REASON_MCS_MEDIA_NOT_CLEAN = "MCS_MEDIA_NOT_CLEAN"
|
||||||
|
|
||||||
|
/* 403 The provided encrypted_body could not be decrypted. The client should request the public key of the server and then retry (once).*/
|
||||||
|
const val REASON_MCS_BAD_DECRYPTION = "MCS_BAD_DECRYPTION"
|
||||||
|
|
||||||
|
/* 400 The request body contains malformed JSON.*/
|
||||||
|
const val REASON_MCS_MALFORMED_JSON = "MCS_MALFORMED_JSON"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScanFailure(val error: ContentScannerError, val httpCode: Int, cause: Throwable? = null) : Throwable(cause = cause)
|
||||||
|
|
||||||
|
// For Glide, which deals with Exception and not with Throwable
|
||||||
|
fun ScanFailure.toException() = Exception(this)
|
||||||
|
fun Throwable.toScanFailure() = this.cause as? ScanFailure
|
@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.api.session.contentscanner
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
|
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
|
||||||
|
|
||||||
|
interface ContentScannerService {
|
||||||
|
|
||||||
|
val serverPublicKey: String?
|
||||||
|
|
||||||
|
fun getContentScannerServer(): String?
|
||||||
|
fun setScannerUrl(url: String?)
|
||||||
|
fun enableScanner(enabled: Boolean)
|
||||||
|
fun isScannerEnabled(): Boolean
|
||||||
|
fun getLiveStatusForFile(mxcUrl: String, fetchIfNeeded: Boolean = true, fileInfo: ElementToDecrypt? = null): LiveData<Optional<ScanStatusInfo>>
|
||||||
|
fun getCachedScanResultForFile(mxcUrl: String): ScanStatusInfo?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current public curve25519 key that the AV server is advertising.
|
||||||
|
* @param callback on success callback containing the server public key
|
||||||
|
*/
|
||||||
|
suspend fun getServerPublicKey(forceDownload: Boolean = false): String?
|
||||||
|
suspend fun getScanResultForAttachment(mxcUrl: String, fileInfo: ElementToDecrypt? = null): ScanStatusInfo
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.api.session.contentscanner
|
||||||
|
|
||||||
|
enum class ScanState {
|
||||||
|
TRUSTED,
|
||||||
|
INFECTED,
|
||||||
|
UNKNOWN,
|
||||||
|
IN_PROGRESS
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ScanStatusInfo(
|
||||||
|
val state: ScanState,
|
||||||
|
val scanDateTimestamp: Long?,
|
||||||
|
val humanReadableMessage: String?
|
||||||
|
)
|
@ -33,6 +33,7 @@ import org.matrix.android.sdk.internal.auth.registration.AddThreePidRegistration
|
|||||||
import org.matrix.android.sdk.internal.auth.registration.RegisterAddThreePidTask
|
import org.matrix.android.sdk.internal.auth.registration.RegisterAddThreePidTask
|
||||||
import org.matrix.android.sdk.internal.network.executeRequest
|
import org.matrix.android.sdk.internal.network.executeRequest
|
||||||
import org.matrix.android.sdk.internal.session.content.DefaultContentUrlResolver
|
import org.matrix.android.sdk.internal.session.content.DefaultContentUrlResolver
|
||||||
|
import org.matrix.android.sdk.internal.session.contentscanner.DisabledContentScannerService
|
||||||
|
|
||||||
internal class DefaultLoginWizard(
|
internal class DefaultLoginWizard(
|
||||||
private val authAPI: AuthAPI,
|
private val authAPI: AuthAPI,
|
||||||
@ -44,7 +45,7 @@ internal class DefaultLoginWizard(
|
|||||||
|
|
||||||
private val getProfileTask: GetProfileTask = DefaultGetProfileTask(
|
private val getProfileTask: GetProfileTask = DefaultGetProfileTask(
|
||||||
authAPI,
|
authAPI,
|
||||||
DefaultContentUrlResolver(pendingSessionData.homeServerConnectionConfig)
|
DefaultContentUrlResolver(pendingSessionData.homeServerConnectionConfig, DisabledContentScannerService())
|
||||||
)
|
)
|
||||||
|
|
||||||
override suspend fun getProfileInfo(matrixId: String): LoginProfileInfo {
|
override suspend fun getProfileInfo(matrixId: String): LoginProfileInfo {
|
||||||
|
@ -37,3 +37,7 @@ internal annotation class CryptoDatabase
|
|||||||
@Qualifier
|
@Qualifier
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
internal annotation class IdentityDatabase
|
internal annotation class IdentityDatabase
|
||||||
|
|
||||||
|
@Qualifier
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
internal annotation class ContentScannerDatabase
|
||||||
|
@ -38,6 +38,9 @@ internal object NetworkConstants {
|
|||||||
// Integration
|
// Integration
|
||||||
const val URI_INTEGRATION_MANAGER_PATH = "_matrix/integrations/v1/"
|
const val URI_INTEGRATION_MANAGER_PATH = "_matrix/integrations/v1/"
|
||||||
|
|
||||||
|
// Content scanner
|
||||||
|
const val URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE = "_matrix/media_proxy/unstable/"
|
||||||
|
|
||||||
// Federation
|
// Federation
|
||||||
const val URI_FEDERATION_PATH = "_matrix/federation/v1/"
|
const val URI_FEDERATION_PATH = "_matrix/federation/v1/"
|
||||||
}
|
}
|
||||||
|
@ -23,8 +23,10 @@ import androidx.core.content.FileProvider
|
|||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.completeWith
|
import kotlinx.coroutines.completeWith
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||||
import org.matrix.android.sdk.api.failure.Failure
|
import org.matrix.android.sdk.api.failure.Failure
|
||||||
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
|
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
|
||||||
@ -118,12 +120,24 @@ internal class DefaultFileService @Inject constructor(
|
|||||||
val cachedFiles = getFiles(url, fileName, mimeType, elementToDecrypt != null)
|
val cachedFiles = getFiles(url, fileName, mimeType, elementToDecrypt != null)
|
||||||
|
|
||||||
if (!cachedFiles.file.exists()) {
|
if (!cachedFiles.file.exists()) {
|
||||||
val resolvedUrl = contentUrlResolver.resolveFullSize(url) ?: throw IllegalArgumentException("url is null")
|
val resolvedUrl = contentUrlResolver.resolveForDownload(url, elementToDecrypt) ?: throw IllegalArgumentException("url is null")
|
||||||
|
|
||||||
val request = Request.Builder()
|
val request = when (resolvedUrl) {
|
||||||
.url(resolvedUrl)
|
is ContentUrlResolver.ResolvedMethod.GET -> {
|
||||||
.header(DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER, url)
|
Request.Builder()
|
||||||
.build()
|
.url(resolvedUrl.url)
|
||||||
|
.header(DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER, url)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
is ContentUrlResolver.ResolvedMethod.POST -> {
|
||||||
|
Request.Builder()
|
||||||
|
.url(resolvedUrl.url)
|
||||||
|
.header(DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER, url)
|
||||||
|
.post(resolvedUrl.jsonBody.toRequestBody("application/json".toMediaType()))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val response = try {
|
val response = try {
|
||||||
okHttpClient.newCall(request).execute()
|
okHttpClient.newCall(request).execute()
|
||||||
|
@ -35,6 +35,7 @@ import org.matrix.android.sdk.api.session.cache.CacheService
|
|||||||
import org.matrix.android.sdk.api.session.call.CallSignalingService
|
import org.matrix.android.sdk.api.session.call.CallSignalingService
|
||||||
import org.matrix.android.sdk.api.session.content.ContentUploadStateTracker
|
import org.matrix.android.sdk.api.session.content.ContentUploadStateTracker
|
||||||
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
|
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
|
||||||
|
import org.matrix.android.sdk.api.session.contentscanner.ContentScannerService
|
||||||
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
import org.matrix.android.sdk.api.session.crypto.CryptoService
|
||||||
import org.matrix.android.sdk.api.session.events.EventService
|
import org.matrix.android.sdk.api.session.events.EventService
|
||||||
import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker
|
import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker
|
||||||
@ -124,6 +125,7 @@ internal class DefaultSession @Inject constructor(
|
|||||||
private val _sharedSecretStorageService: Lazy<SharedSecretStorageService>,
|
private val _sharedSecretStorageService: Lazy<SharedSecretStorageService>,
|
||||||
private val accountService: Lazy<AccountService>,
|
private val accountService: Lazy<AccountService>,
|
||||||
private val eventService: Lazy<EventService>,
|
private val eventService: Lazy<EventService>,
|
||||||
|
private val contentScannerService: Lazy<ContentScannerService>,
|
||||||
private val identityService: IdentityService,
|
private val identityService: IdentityService,
|
||||||
private val integrationManagerService: IntegrationManagerService,
|
private val integrationManagerService: IntegrationManagerService,
|
||||||
private val thirdPartyService: Lazy<ThirdPartyService>,
|
private val thirdPartyService: Lazy<ThirdPartyService>,
|
||||||
@ -275,6 +277,8 @@ internal class DefaultSession @Inject constructor(
|
|||||||
|
|
||||||
override fun cryptoService(): CryptoService = cryptoService.get()
|
override fun cryptoService(): CryptoService = cryptoService.get()
|
||||||
|
|
||||||
|
override fun contentScannerService(): ContentScannerService = contentScannerService.get()
|
||||||
|
|
||||||
override fun identityService() = identityService
|
override fun identityService() = identityService
|
||||||
|
|
||||||
override fun fileService(): FileService = defaultFileService.get()
|
override fun fileService(): FileService = defaultFileService.get()
|
||||||
|
@ -36,6 +36,7 @@ import org.matrix.android.sdk.internal.session.cache.CacheModule
|
|||||||
import org.matrix.android.sdk.internal.session.call.CallModule
|
import org.matrix.android.sdk.internal.session.call.CallModule
|
||||||
import org.matrix.android.sdk.internal.session.content.ContentModule
|
import org.matrix.android.sdk.internal.session.content.ContentModule
|
||||||
import org.matrix.android.sdk.internal.session.content.UploadContentWorker
|
import org.matrix.android.sdk.internal.session.content.UploadContentWorker
|
||||||
|
import org.matrix.android.sdk.internal.session.contentscanner.ContentScannerModule
|
||||||
import org.matrix.android.sdk.internal.session.filter.FilterModule
|
import org.matrix.android.sdk.internal.session.filter.FilterModule
|
||||||
import org.matrix.android.sdk.internal.session.group.GetGroupDataWorker
|
import org.matrix.android.sdk.internal.session.group.GetGroupDataWorker
|
||||||
import org.matrix.android.sdk.internal.session.group.GroupModule
|
import org.matrix.android.sdk.internal.session.group.GroupModule
|
||||||
@ -94,6 +95,7 @@ import org.matrix.android.sdk.internal.util.system.SystemModule
|
|||||||
AccountModule::class,
|
AccountModule::class,
|
||||||
FederationModule::class,
|
FederationModule::class,
|
||||||
CallModule::class,
|
CallModule::class,
|
||||||
|
ContentScannerModule::class,
|
||||||
SearchModule::class,
|
SearchModule::class,
|
||||||
ThirdPartyModule::class,
|
ThirdPartyModule::class,
|
||||||
SpaceModule::class,
|
SpaceModule::class,
|
||||||
|
@ -20,16 +20,41 @@ import org.matrix.android.sdk.api.MatrixUrls
|
|||||||
import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl
|
import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl
|
||||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||||
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
|
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
|
||||||
|
import org.matrix.android.sdk.api.session.contentscanner.ContentScannerService
|
||||||
|
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
|
||||||
import org.matrix.android.sdk.internal.network.NetworkConstants
|
import org.matrix.android.sdk.internal.network.NetworkConstants
|
||||||
|
import org.matrix.android.sdk.internal.session.contentscanner.ScanEncryptorUtils
|
||||||
|
import org.matrix.android.sdk.internal.session.contentscanner.model.toJson
|
||||||
import org.matrix.android.sdk.internal.util.ensureTrailingSlash
|
import org.matrix.android.sdk.internal.util.ensureTrailingSlash
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class DefaultContentUrlResolver @Inject constructor(homeServerConnectionConfig: HomeServerConnectionConfig) : ContentUrlResolver {
|
internal class DefaultContentUrlResolver @Inject constructor(
|
||||||
|
homeServerConnectionConfig: HomeServerConnectionConfig,
|
||||||
|
private val scannerService: ContentScannerService
|
||||||
|
) : ContentUrlResolver {
|
||||||
|
|
||||||
private val baseUrl = homeServerConnectionConfig.homeServerUriBase.toString().ensureTrailingSlash()
|
private val baseUrl = homeServerConnectionConfig.homeServerUriBase.toString().ensureTrailingSlash()
|
||||||
|
|
||||||
override val uploadUrl = baseUrl + NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "upload"
|
override val uploadUrl = baseUrl + NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "upload"
|
||||||
|
|
||||||
|
override fun resolveForDownload(contentUrl: String?, elementToDecrypt: ElementToDecrypt?): ContentUrlResolver.ResolvedMethod? {
|
||||||
|
return if (scannerService.isScannerEnabled() && elementToDecrypt != null) {
|
||||||
|
val baseUrl = scannerService.getContentScannerServer()
|
||||||
|
val sep = if (baseUrl?.endsWith("/") == true) "" else "/"
|
||||||
|
|
||||||
|
val url = baseUrl + sep + NetworkConstants.URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE + "download_encrypted"
|
||||||
|
|
||||||
|
ContentUrlResolver.ResolvedMethod.POST(
|
||||||
|
url = url,
|
||||||
|
jsonBody = ScanEncryptorUtils
|
||||||
|
.getDownloadBodyAndEncryptIfNeeded(scannerService.serverPublicKey, contentUrl ?: "", elementToDecrypt)
|
||||||
|
.toJson()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
resolveFullSize(contentUrl)?.let { ContentUrlResolver.ResolvedMethod.GET(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun resolveFullSize(contentUrl: String?): String? {
|
override fun resolveFullSize(contentUrl: String?): String? {
|
||||||
return contentUrl
|
return contentUrl
|
||||||
// do not allow non-mxc content URLs
|
// do not allow non-mxc content URLs
|
||||||
@ -37,7 +62,7 @@ internal class DefaultContentUrlResolver @Inject constructor(homeServerConnectio
|
|||||||
?.let {
|
?.let {
|
||||||
resolve(
|
resolve(
|
||||||
contentUrl = it,
|
contentUrl = it,
|
||||||
prefix = NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "download/"
|
toThumbnail = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -49,16 +74,27 @@ internal class DefaultContentUrlResolver @Inject constructor(homeServerConnectio
|
|||||||
?.let {
|
?.let {
|
||||||
resolve(
|
resolve(
|
||||||
contentUrl = it,
|
contentUrl = it,
|
||||||
prefix = NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "thumbnail/",
|
toThumbnail = true,
|
||||||
params = "?width=$width&height=$height&method=${method.value}"
|
params = "?width=$width&height=$height&method=${method.value}"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resolve(contentUrl: String,
|
private fun resolve(contentUrl: String,
|
||||||
prefix: String,
|
toThumbnail: Boolean,
|
||||||
params: String = ""): String? {
|
params: String = ""): String? {
|
||||||
var serverAndMediaId = contentUrl.removePrefix(MatrixUrls.MATRIX_CONTENT_URI_SCHEME)
|
var serverAndMediaId = contentUrl.removePrefix(MatrixUrls.MATRIX_CONTENT_URI_SCHEME)
|
||||||
|
|
||||||
|
val apiPath = if (scannerService.isScannerEnabled()) {
|
||||||
|
NetworkConstants.URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE
|
||||||
|
} else {
|
||||||
|
NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0
|
||||||
|
}
|
||||||
|
val prefix = if (toThumbnail) {
|
||||||
|
apiPath + "thumbnail/"
|
||||||
|
} else {
|
||||||
|
apiPath + "download/"
|
||||||
|
}
|
||||||
val fragmentOffset = serverAndMediaId.indexOf("#")
|
val fragmentOffset = serverAndMediaId.indexOf("#")
|
||||||
var fragment = ""
|
var fragment = ""
|
||||||
if (fragmentOffset >= 0) {
|
if (fragmentOffset >= 0) {
|
||||||
@ -66,6 +102,11 @@ internal class DefaultContentUrlResolver @Inject constructor(homeServerConnectio
|
|||||||
serverAndMediaId = serverAndMediaId.substring(0, fragmentOffset)
|
serverAndMediaId = serverAndMediaId.substring(0, fragmentOffset)
|
||||||
}
|
}
|
||||||
|
|
||||||
return baseUrl + prefix + serverAndMediaId + params + fragment
|
val resolvedUrl = if (scannerService.isScannerEnabled()) {
|
||||||
|
scannerService.getContentScannerServer()!!.ensureTrailingSlash()
|
||||||
|
} else {
|
||||||
|
baseUrl
|
||||||
|
}
|
||||||
|
return resolvedUrl + prefix + serverAndMediaId + params + fragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.contentscanner
|
||||||
|
|
||||||
|
import okhttp3.ResponseBody
|
||||||
|
import org.matrix.android.sdk.internal.network.NetworkConstants
|
||||||
|
import org.matrix.android.sdk.internal.session.contentscanner.model.DownloadBody
|
||||||
|
import org.matrix.android.sdk.internal.session.contentscanner.model.ScanResponse
|
||||||
|
import org.matrix.android.sdk.internal.session.contentscanner.model.ServerPublicKeyResponse
|
||||||
|
import retrofit2.http.Body
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.POST
|
||||||
|
import retrofit2.http.Path
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/matrix-org/matrix-content-scanner
|
||||||
|
*/
|
||||||
|
internal interface ContentScannerApi {
|
||||||
|
|
||||||
|
@POST(NetworkConstants.URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE + "download_encrypted")
|
||||||
|
suspend fun downloadEncrypted(@Body info: DownloadBody): ResponseBody
|
||||||
|
|
||||||
|
@POST(NetworkConstants.URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE + "scan_encrypted")
|
||||||
|
suspend fun scanFile(@Body info: DownloadBody): ScanResponse
|
||||||
|
|
||||||
|
@GET(NetworkConstants.URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE + "public_key")
|
||||||
|
suspend fun getServerPublicKey(): ServerPublicKeyResponse
|
||||||
|
|
||||||
|
@GET(NetworkConstants.URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE + "scan/{domain}/{mediaId}")
|
||||||
|
suspend fun scanMedia(@Path(value = "domain") domain: String, @Path(value = "mediaId") mediaId: String): ScanResponse
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.contentscanner
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.internal.session.SessionScope
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@SessionScope
|
||||||
|
internal class ContentScannerApiProvider @Inject constructor() {
|
||||||
|
var contentScannerApi: ContentScannerApi? = null
|
||||||
|
}
|
@ -0,0 +1,84 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.contentscanner
|
||||||
|
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import io.realm.RealmConfiguration
|
||||||
|
import org.matrix.android.sdk.api.session.contentscanner.ContentScannerService
|
||||||
|
import org.matrix.android.sdk.internal.database.RealmKeysUtils
|
||||||
|
import org.matrix.android.sdk.internal.di.ContentScannerDatabase
|
||||||
|
import org.matrix.android.sdk.internal.di.SessionFilesDirectory
|
||||||
|
import org.matrix.android.sdk.internal.di.UserMd5
|
||||||
|
import org.matrix.android.sdk.internal.session.SessionModule
|
||||||
|
import org.matrix.android.sdk.internal.session.SessionScope
|
||||||
|
import org.matrix.android.sdk.internal.session.contentscanner.data.ContentScannerStore
|
||||||
|
import org.matrix.android.sdk.internal.session.contentscanner.db.ContentScannerRealmModule
|
||||||
|
import org.matrix.android.sdk.internal.session.contentscanner.db.RealmContentScannerStore
|
||||||
|
import org.matrix.android.sdk.internal.session.contentscanner.tasks.DefaultDownloadEncryptedTask
|
||||||
|
import org.matrix.android.sdk.internal.session.contentscanner.tasks.DefaultGetServerPublicKeyTask
|
||||||
|
import org.matrix.android.sdk.internal.session.contentscanner.tasks.DefaultScanEncryptedTask
|
||||||
|
import org.matrix.android.sdk.internal.session.contentscanner.tasks.DefaultScanMediaTask
|
||||||
|
import org.matrix.android.sdk.internal.session.contentscanner.tasks.DownloadEncryptedTask
|
||||||
|
import org.matrix.android.sdk.internal.session.contentscanner.tasks.GetServerPublicKeyTask
|
||||||
|
import org.matrix.android.sdk.internal.session.contentscanner.tasks.ScanEncryptedTask
|
||||||
|
import org.matrix.android.sdk.internal.session.contentscanner.tasks.ScanMediaTask
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
@Module
|
||||||
|
internal abstract class ContentScannerModule {
|
||||||
|
@Module
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@Provides
|
||||||
|
@ContentScannerDatabase
|
||||||
|
@SessionScope
|
||||||
|
fun providesContentScannerRealmConfiguration(realmKeysUtils: RealmKeysUtils,
|
||||||
|
@SessionFilesDirectory directory: File,
|
||||||
|
@UserMd5 userMd5: String): RealmConfiguration {
|
||||||
|
return RealmConfiguration.Builder()
|
||||||
|
.directory(directory)
|
||||||
|
.name("matrix-sdk-content-scanning.realm")
|
||||||
|
.apply {
|
||||||
|
realmKeysUtils.configureEncryption(this, SessionModule.getKeyAlias(userMd5))
|
||||||
|
}
|
||||||
|
.allowWritesOnUiThread(true)
|
||||||
|
.modules(ContentScannerRealmModule())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindContentScannerService(service: DisabledContentScannerService): ContentScannerService
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindContentScannerStore(store: RealmContentScannerStore): ContentScannerStore
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindDownloadEncryptedTask(task: DefaultDownloadEncryptedTask): DownloadEncryptedTask
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindGetServerPublicKeyTask(task: DefaultGetServerPublicKeyTask): GetServerPublicKeyTask
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindScanMediaTask(task: DefaultScanMediaTask): ScanMediaTask
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindScanEncryptedTask(task: DefaultScanEncryptedTask): ScanEncryptedTask
|
||||||
|
}
|
@ -0,0 +1,131 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.contentscanner
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import dagger.Lazy
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import org.matrix.android.sdk.api.session.contentscanner.ContentScannerService
|
||||||
|
import org.matrix.android.sdk.api.session.contentscanner.ScanState
|
||||||
|
import org.matrix.android.sdk.api.session.contentscanner.ScanStatusInfo
|
||||||
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
|
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
|
||||||
|
import org.matrix.android.sdk.internal.di.Unauthenticated
|
||||||
|
import org.matrix.android.sdk.internal.network.RetrofitFactory
|
||||||
|
import org.matrix.android.sdk.internal.session.SessionScope
|
||||||
|
import org.matrix.android.sdk.internal.session.contentscanner.data.ContentScannerStore
|
||||||
|
import org.matrix.android.sdk.internal.session.contentscanner.tasks.GetServerPublicKeyTask
|
||||||
|
import org.matrix.android.sdk.internal.session.contentscanner.tasks.ScanEncryptedTask
|
||||||
|
import org.matrix.android.sdk.internal.session.contentscanner.tasks.ScanMediaTask
|
||||||
|
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@SessionScope
|
||||||
|
internal class DefaultContentScannerService @Inject constructor(
|
||||||
|
private val retrofitFactory: RetrofitFactory,
|
||||||
|
@Unauthenticated
|
||||||
|
private val okHttpClient: Lazy<OkHttpClient>,
|
||||||
|
private val contentScannerApiProvider: ContentScannerApiProvider,
|
||||||
|
private val contentScannerStore: ContentScannerStore,
|
||||||
|
private val getServerPublicKeyTask: GetServerPublicKeyTask,
|
||||||
|
private val scanEncryptedTask: ScanEncryptedTask,
|
||||||
|
private val scanMediaTask: ScanMediaTask,
|
||||||
|
private val taskExecutor: TaskExecutor
|
||||||
|
) : ContentScannerService {
|
||||||
|
|
||||||
|
// Cache public key in memory
|
||||||
|
override var serverPublicKey: String? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
override fun getContentScannerServer(): String? {
|
||||||
|
return contentScannerStore.getScannerUrl()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getServerPublicKey(forceDownload: Boolean): String? {
|
||||||
|
val api = contentScannerApiProvider.contentScannerApi ?: throw IllegalArgumentException("No content scanner define")
|
||||||
|
|
||||||
|
if (!forceDownload && serverPublicKey != null) {
|
||||||
|
return serverPublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
return getServerPublicKeyTask.execute(GetServerPublicKeyTask.Params(api)).also {
|
||||||
|
serverPublicKey = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getScanResultForAttachment(mxcUrl: String, fileInfo: ElementToDecrypt?): ScanStatusInfo {
|
||||||
|
val result = if (fileInfo != null) {
|
||||||
|
scanEncryptedTask.execute(ScanEncryptedTask.Params(
|
||||||
|
mxcUrl = mxcUrl,
|
||||||
|
publicServerKey = getServerPublicKey(false),
|
||||||
|
encryptedInfo = fileInfo
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
scanMediaTask.execute(ScanMediaTask.Params(mxcUrl))
|
||||||
|
}
|
||||||
|
|
||||||
|
return ScanStatusInfo(
|
||||||
|
state = if (result.clean) ScanState.TRUSTED else ScanState.INFECTED,
|
||||||
|
humanReadableMessage = result.info,
|
||||||
|
scanDateTimestamp = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setScannerUrl(url: String?) = contentScannerStore.setScannerUrl(url).also {
|
||||||
|
if (url == null) {
|
||||||
|
contentScannerApiProvider.contentScannerApi = null
|
||||||
|
serverPublicKey = null
|
||||||
|
} else {
|
||||||
|
val api = retrofitFactory
|
||||||
|
.create(okHttpClient, url)
|
||||||
|
.create(ContentScannerApi::class.java)
|
||||||
|
contentScannerApiProvider.contentScannerApi = api
|
||||||
|
|
||||||
|
taskExecutor.executorScope.launch {
|
||||||
|
try {
|
||||||
|
getServerPublicKey(true)
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
Timber.e("Failed to get public server api")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun enableScanner(enabled: Boolean) = contentScannerStore.enableScanner(enabled)
|
||||||
|
|
||||||
|
override fun isScannerEnabled(): Boolean = contentScannerStore.isScanEnabled()
|
||||||
|
|
||||||
|
override fun getCachedScanResultForFile(mxcUrl: String): ScanStatusInfo? {
|
||||||
|
return contentScannerStore.getScanResult(mxcUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLiveStatusForFile(mxcUrl: String, fetchIfNeeded: Boolean, fileInfo: ElementToDecrypt?): LiveData<Optional<ScanStatusInfo>> {
|
||||||
|
val data = contentScannerStore.getLiveScanResult(mxcUrl)
|
||||||
|
if (fetchIfNeeded && !contentScannerStore.isScanResultKnownOrInProgress(mxcUrl, getContentScannerServer())) {
|
||||||
|
taskExecutor.executorScope.launch {
|
||||||
|
try {
|
||||||
|
getScanResultForAttachment(mxcUrl, fileInfo)
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
Timber.e("Failed to get file status : ${failure.localizedMessage}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.contentscanner
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import org.matrix.android.sdk.api.session.contentscanner.ContentScannerService
|
||||||
|
import org.matrix.android.sdk.api.session.contentscanner.ScanStatusInfo
|
||||||
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
|
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
|
||||||
|
import org.matrix.android.sdk.internal.session.SessionScope
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created to by-pass ProfileTask execution in LoginWizard.
|
||||||
|
*/
|
||||||
|
@SessionScope
|
||||||
|
internal class DisabledContentScannerService @Inject constructor() : ContentScannerService {
|
||||||
|
|
||||||
|
override val serverPublicKey: String?
|
||||||
|
get() = null
|
||||||
|
|
||||||
|
override fun getContentScannerServer(): String? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getServerPublicKey(forceDownload: Boolean): String? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getScanResultForAttachment(mxcUrl: String, fileInfo: ElementToDecrypt?): ScanStatusInfo {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setScannerUrl(url: String?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun enableScanner(enabled: Boolean) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isScannerEnabled(): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLiveStatusForFile(mxcUrl: String, fetchIfNeeded: Boolean, fileInfo: ElementToDecrypt?): LiveData<Optional<ScanStatusInfo>> {
|
||||||
|
return MutableLiveData()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCachedScanResultForFile(mxcUrl: String): ScanStatusInfo? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.contentscanner
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
|
||||||
|
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
|
||||||
|
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileKey
|
||||||
|
import org.matrix.android.sdk.internal.crypto.tools.withOlmEncryption
|
||||||
|
import org.matrix.android.sdk.internal.session.contentscanner.model.DownloadBody
|
||||||
|
import org.matrix.android.sdk.internal.session.contentscanner.model.EncryptedBody
|
||||||
|
import org.matrix.android.sdk.internal.session.contentscanner.model.toCanonicalJson
|
||||||
|
|
||||||
|
internal object ScanEncryptorUtils {
|
||||||
|
|
||||||
|
fun getDownloadBodyAndEncryptIfNeeded(publicServerKey: String?, mxcUrl: String, elementToDecrypt: ElementToDecrypt): DownloadBody {
|
||||||
|
// TODO, upstream refactoring changed the object model here...
|
||||||
|
// it's bad we have to recreate and use hardcoded values
|
||||||
|
val encryptedInfo = EncryptedFileInfo(
|
||||||
|
url = mxcUrl,
|
||||||
|
iv = elementToDecrypt.iv,
|
||||||
|
hashes = mapOf("sha256" to elementToDecrypt.sha256),
|
||||||
|
key = EncryptedFileKey(
|
||||||
|
k = elementToDecrypt.k,
|
||||||
|
alg = "A256CTR",
|
||||||
|
keyOps = listOf("encrypt", "decrypt"),
|
||||||
|
kty = "oct",
|
||||||
|
ext = true
|
||||||
|
),
|
||||||
|
v = "v2"
|
||||||
|
)
|
||||||
|
return if (publicServerKey != null) {
|
||||||
|
// We should encrypt
|
||||||
|
withOlmEncryption { olm ->
|
||||||
|
olm.setRecipientKey(publicServerKey)
|
||||||
|
|
||||||
|
val olmResult = olm.encrypt(DownloadBody(encryptedInfo).toCanonicalJson())
|
||||||
|
DownloadBody(
|
||||||
|
encryptedBody = EncryptedBody(
|
||||||
|
cipherText = olmResult.mCipherText,
|
||||||
|
ephemeral = olmResult.mEphemeralKey,
|
||||||
|
mac = olmResult.mMac
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DownloadBody(encryptedInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.contentscanner.data
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import org.matrix.android.sdk.api.session.contentscanner.ScanState
|
||||||
|
import org.matrix.android.sdk.api.session.contentscanner.ScanStatusInfo
|
||||||
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
|
|
||||||
|
internal interface ContentScannerStore {
|
||||||
|
|
||||||
|
fun getScannerUrl(): String?
|
||||||
|
|
||||||
|
fun setScannerUrl(url: String?)
|
||||||
|
|
||||||
|
fun enableScanner(enabled: Boolean)
|
||||||
|
|
||||||
|
fun isScanEnabled(): Boolean
|
||||||
|
|
||||||
|
fun getScanResult(mxcUrl: String): ScanStatusInfo?
|
||||||
|
fun getLiveScanResult(mxcUrl: String): LiveData<Optional<ScanStatusInfo>>
|
||||||
|
fun isScanResultKnownOrInProgress(mxcUrl: String, scannerUrl: String?): Boolean
|
||||||
|
|
||||||
|
fun updateStateForContent(mxcUrl: String, state: ScanState, scannerUrl: String?)
|
||||||
|
fun updateScanResultForContent(mxcUrl: String, scannerUrl: String?, state: ScanState, humanReadable: String)
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.contentscanner.db
|
||||||
|
|
||||||
|
import io.realm.RealmObject
|
||||||
|
import io.realm.annotations.Index
|
||||||
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
|
import org.matrix.android.sdk.api.session.contentscanner.ScanState
|
||||||
|
import org.matrix.android.sdk.api.session.contentscanner.ScanStatusInfo
|
||||||
|
|
||||||
|
internal open class ContentScanResultEntity(
|
||||||
|
@Index
|
||||||
|
var mediaUrl: String? = null,
|
||||||
|
var scanStatusString: String? = null,
|
||||||
|
var humanReadableMessage: String? = null,
|
||||||
|
var scanDateTimestamp: Long? = null,
|
||||||
|
var scannerUrl: String? = null
|
||||||
|
) : RealmObject() {
|
||||||
|
|
||||||
|
var scanResult: ScanState
|
||||||
|
get() {
|
||||||
|
return scanStatusString
|
||||||
|
?.let {
|
||||||
|
tryOrNull { ScanState.valueOf(it) }
|
||||||
|
}
|
||||||
|
?: ScanState.UNKNOWN
|
||||||
|
}
|
||||||
|
set(result) {
|
||||||
|
scanStatusString = result.name
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toModel(): ScanStatusInfo {
|
||||||
|
return ScanStatusInfo(
|
||||||
|
state = this.scanResult,
|
||||||
|
humanReadableMessage = humanReadableMessage,
|
||||||
|
scanDateTimestamp = scanDateTimestamp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.contentscanner.db
|
||||||
|
|
||||||
|
import io.realm.Realm
|
||||||
|
import io.realm.kotlin.createObject
|
||||||
|
import io.realm.kotlin.where
|
||||||
|
|
||||||
|
internal fun ContentScanResultEntity.Companion.get(realm: Realm, attachmentUrl: String, contentScannerUrl: String?): ContentScanResultEntity? {
|
||||||
|
return realm.where<ContentScanResultEntity>()
|
||||||
|
.equalTo(ContentScanResultEntityFields.MEDIA_URL, attachmentUrl)
|
||||||
|
.apply {
|
||||||
|
contentScannerUrl?.let {
|
||||||
|
equalTo(ContentScanResultEntityFields.SCANNER_URL, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.findFirst()
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun ContentScanResultEntity.Companion.getOrCreate(realm: Realm, attachmentUrl: String, contentScannerUrl: String?): ContentScanResultEntity {
|
||||||
|
return ContentScanResultEntity.get(realm, attachmentUrl, contentScannerUrl)
|
||||||
|
?: realm.createObject<ContentScanResultEntity>().also {
|
||||||
|
it.mediaUrl = attachmentUrl
|
||||||
|
it.scanDateTimestamp = System.currentTimeMillis()
|
||||||
|
it.scannerUrl = contentScannerUrl
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.contentscanner.db
|
||||||
|
|
||||||
|
import io.realm.RealmObject
|
||||||
|
|
||||||
|
internal open class ContentScannerInfoEntity(
|
||||||
|
var serverUrl: String? = null,
|
||||||
|
var enabled: Boolean? = null
|
||||||
|
) : RealmObject() {
|
||||||
|
|
||||||
|
companion object
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.contentscanner.db
|
||||||
|
|
||||||
|
import io.realm.annotations.RealmModule
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Realm module for content scanner classes
|
||||||
|
*/
|
||||||
|
@RealmModule(library = true,
|
||||||
|
classes = [
|
||||||
|
ContentScannerInfoEntity::class,
|
||||||
|
ContentScanResultEntity::class
|
||||||
|
])
|
||||||
|
internal class ContentScannerRealmModule
|
@ -0,0 +1,143 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.contentscanner.db
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.Transformations
|
||||||
|
import com.zhuinden.monarchy.Monarchy
|
||||||
|
import io.realm.Realm
|
||||||
|
import io.realm.RealmConfiguration
|
||||||
|
import io.realm.kotlin.createObject
|
||||||
|
import io.realm.kotlin.where
|
||||||
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
|
import org.matrix.android.sdk.api.session.contentscanner.ScanState
|
||||||
|
import org.matrix.android.sdk.api.session.contentscanner.ScanStatusInfo
|
||||||
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
|
import org.matrix.android.sdk.api.util.toOptional
|
||||||
|
import org.matrix.android.sdk.internal.di.ContentScannerDatabase
|
||||||
|
import org.matrix.android.sdk.internal.session.SessionScope
|
||||||
|
import org.matrix.android.sdk.internal.session.contentscanner.data.ContentScannerStore
|
||||||
|
import org.matrix.android.sdk.internal.util.isValidUrl
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@SessionScope
|
||||||
|
internal class RealmContentScannerStore @Inject constructor(
|
||||||
|
@ContentScannerDatabase
|
||||||
|
private val realmConfiguration: RealmConfiguration
|
||||||
|
) : ContentScannerStore {
|
||||||
|
|
||||||
|
private val monarchy = Monarchy.Builder()
|
||||||
|
.setRealmConfiguration(realmConfiguration)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun getScannerUrl(): String? {
|
||||||
|
return monarchy.fetchAllMappedSync(
|
||||||
|
{ realm ->
|
||||||
|
realm.where<ContentScannerInfoEntity>()
|
||||||
|
}, {
|
||||||
|
it.serverUrl
|
||||||
|
}
|
||||||
|
).firstOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setScannerUrl(url: String?) {
|
||||||
|
monarchy.runTransactionSync { realm ->
|
||||||
|
val info = realm.where<ContentScannerInfoEntity>().findFirst()
|
||||||
|
?: realm.createObject()
|
||||||
|
info.serverUrl = url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun enableScanner(enabled: Boolean) {
|
||||||
|
monarchy.runTransactionSync { realm ->
|
||||||
|
val info = realm.where<ContentScannerInfoEntity>().findFirst()
|
||||||
|
?: realm.createObject()
|
||||||
|
info.enabled = enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isScanEnabled(): Boolean {
|
||||||
|
return monarchy.fetchAllMappedSync(
|
||||||
|
{ realm ->
|
||||||
|
realm.where<ContentScannerInfoEntity>()
|
||||||
|
}, {
|
||||||
|
it.enabled.orFalse() && it.serverUrl?.isValidUrl().orFalse()
|
||||||
|
}
|
||||||
|
).firstOrNull().orFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateStateForContent(mxcUrl: String, state: ScanState, scannerUrl: String?) {
|
||||||
|
monarchy.runTransactionSync {
|
||||||
|
ContentScanResultEntity.getOrCreate(it, mxcUrl, scannerUrl).scanResult = state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateScanResultForContent(mxcUrl: String, scannerUrl: String?, state: ScanState, humanReadable: String) {
|
||||||
|
monarchy.runTransactionSync {
|
||||||
|
ContentScanResultEntity.getOrCreate(it, mxcUrl, scannerUrl).apply {
|
||||||
|
scanResult = state
|
||||||
|
scanDateTimestamp = System.currentTimeMillis()
|
||||||
|
humanReadableMessage = humanReadable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isScanResultKnownOrInProgress(mxcUrl: String, scannerUrl: String?): Boolean {
|
||||||
|
var isKnown = false
|
||||||
|
monarchy.runTransactionSync {
|
||||||
|
val info = ContentScanResultEntity.get(it, mxcUrl, scannerUrl)?.scanResult
|
||||||
|
isKnown = when (info) {
|
||||||
|
ScanState.IN_PROGRESS,
|
||||||
|
ScanState.TRUSTED,
|
||||||
|
ScanState.INFECTED -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return isKnown
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getScanResult(mxcUrl: String): ScanStatusInfo? {
|
||||||
|
return monarchy.fetchAllMappedSync({ realm ->
|
||||||
|
realm.where<ContentScanResultEntity>()
|
||||||
|
.equalTo(ContentScanResultEntityFields.MEDIA_URL, mxcUrl)
|
||||||
|
.apply {
|
||||||
|
getScannerUrl()?.let {
|
||||||
|
equalTo(ContentScanResultEntityFields.SCANNER_URL, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
it.toModel()
|
||||||
|
})
|
||||||
|
.firstOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLiveScanResult(mxcUrl: String): LiveData<Optional<ScanStatusInfo>> {
|
||||||
|
val liveData = monarchy.findAllMappedWithChanges(
|
||||||
|
{ realm: Realm ->
|
||||||
|
realm.where<ContentScanResultEntity>()
|
||||||
|
.equalTo(ContentScanResultEntityFields.MEDIA_URL, mxcUrl)
|
||||||
|
.equalTo(ContentScanResultEntityFields.SCANNER_URL, getScannerUrl())
|
||||||
|
},
|
||||||
|
{ entity ->
|
||||||
|
entity.toModel()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return Transformations.map(liveData) {
|
||||||
|
it.firstOrNull().toOptional()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.contentscanner.model
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
|
||||||
|
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||||
|
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
internal data class DownloadBody(
|
||||||
|
@Json(name = "file") val file: EncryptedFileInfo? = null,
|
||||||
|
@Json(name = "encrypted_body") val encryptedBody: EncryptedBody? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
internal data class EncryptedBody(
|
||||||
|
@Json(name = "ciphertext") val cipherText: String,
|
||||||
|
@Json(name = "mac") val mac: String,
|
||||||
|
@Json(name = "ephemeral") val ephemeral: String
|
||||||
|
)
|
||||||
|
|
||||||
|
internal fun DownloadBody.toJson(): String = MoshiProvider.providesMoshi().adapter(DownloadBody::class.java).toJson(this)
|
||||||
|
|
||||||
|
internal fun DownloadBody.toCanonicalJson() = JsonCanonicalizer.getCanonicalJson(DownloadBody::class.java, this)
|
@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.contentscanner.model
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {
|
||||||
|
* "clean": true,
|
||||||
|
* "info": "File clean at 6/7/2018, 6:02:40 PM"
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
internal data class ScanResponse(
|
||||||
|
@Json(name = "clean") val clean: Boolean,
|
||||||
|
/** Human-readable information about the result. */
|
||||||
|
@Json(name = "info") val info: String?
|
||||||
|
)
|
@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.contentscanner.model
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
internal data class ServerPublicKeyResponse(
|
||||||
|
@Json(name = "public_key")
|
||||||
|
val publicKey: String?
|
||||||
|
)
|
@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.contentscanner.tasks
|
||||||
|
|
||||||
|
import okhttp3.ResponseBody
|
||||||
|
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
|
||||||
|
import org.matrix.android.sdk.internal.network.executeRequest
|
||||||
|
import org.matrix.android.sdk.internal.session.contentscanner.ContentScannerApiProvider
|
||||||
|
import org.matrix.android.sdk.internal.session.contentscanner.ScanEncryptorUtils
|
||||||
|
import org.matrix.android.sdk.internal.task.Task
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal interface DownloadEncryptedTask : Task<DownloadEncryptedTask.Params, ResponseBody> {
|
||||||
|
data class Params(
|
||||||
|
val publicServerKey: String?,
|
||||||
|
val encryptedInfo: ElementToDecrypt,
|
||||||
|
val mxcUrl: String
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class DefaultDownloadEncryptedTask @Inject constructor(
|
||||||
|
private val contentScannerApiProvider: ContentScannerApiProvider
|
||||||
|
) : DownloadEncryptedTask {
|
||||||
|
|
||||||
|
override suspend fun execute(params: DownloadEncryptedTask.Params): ResponseBody {
|
||||||
|
val dlBody = ScanEncryptorUtils.getDownloadBodyAndEncryptIfNeeded(
|
||||||
|
params.publicServerKey,
|
||||||
|
params.mxcUrl,
|
||||||
|
params.encryptedInfo
|
||||||
|
)
|
||||||
|
|
||||||
|
val api = contentScannerApiProvider.contentScannerApi ?: throw IllegalArgumentException()
|
||||||
|
return executeRequest(null) {
|
||||||
|
api.downloadEncrypted(dlBody)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.contentscanner.tasks
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.internal.network.executeRequest
|
||||||
|
import org.matrix.android.sdk.internal.session.contentscanner.ContentScannerApi
|
||||||
|
import org.matrix.android.sdk.internal.session.contentscanner.model.ServerPublicKeyResponse
|
||||||
|
import org.matrix.android.sdk.internal.task.Task
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal interface GetServerPublicKeyTask : Task<GetServerPublicKeyTask.Params, String?> {
|
||||||
|
data class Params(
|
||||||
|
val contentScannerApi: ContentScannerApi
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class DefaultGetServerPublicKeyTask @Inject constructor() : GetServerPublicKeyTask {
|
||||||
|
|
||||||
|
override suspend fun execute(params: GetServerPublicKeyTask.Params): String? {
|
||||||
|
return executeRequest<ServerPublicKeyResponse>(null) {
|
||||||
|
params.contentScannerApi.getServerPublicKey()
|
||||||
|
}.publicKey
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.contentscanner.tasks
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.failure.toScanFailure
|
||||||
|
import org.matrix.android.sdk.api.session.contentscanner.ScanState
|
||||||
|
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
|
||||||
|
import org.matrix.android.sdk.internal.network.executeRequest
|
||||||
|
import org.matrix.android.sdk.internal.session.contentscanner.ContentScannerApiProvider
|
||||||
|
import org.matrix.android.sdk.internal.session.contentscanner.ScanEncryptorUtils
|
||||||
|
import org.matrix.android.sdk.internal.session.contentscanner.data.ContentScannerStore
|
||||||
|
import org.matrix.android.sdk.internal.session.contentscanner.model.ScanResponse
|
||||||
|
import org.matrix.android.sdk.internal.task.Task
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal interface ScanEncryptedTask : Task<ScanEncryptedTask.Params, ScanResponse> {
|
||||||
|
data class Params(
|
||||||
|
val mxcUrl: String,
|
||||||
|
val publicServerKey: String?,
|
||||||
|
val encryptedInfo: ElementToDecrypt
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class DefaultScanEncryptedTask @Inject constructor(
|
||||||
|
private val contentScannerApiProvider: ContentScannerApiProvider,
|
||||||
|
private val contentScannerStore: ContentScannerStore
|
||||||
|
) : ScanEncryptedTask {
|
||||||
|
|
||||||
|
override suspend fun execute(params: ScanEncryptedTask.Params): ScanResponse {
|
||||||
|
val mxcUrl = params.mxcUrl
|
||||||
|
val dlBody = ScanEncryptorUtils.getDownloadBodyAndEncryptIfNeeded(params.publicServerKey, params.mxcUrl, params.encryptedInfo)
|
||||||
|
|
||||||
|
val scannerUrl = contentScannerStore.getScannerUrl()
|
||||||
|
contentScannerStore.updateStateForContent(params.mxcUrl, ScanState.IN_PROGRESS, scannerUrl)
|
||||||
|
|
||||||
|
try {
|
||||||
|
val api = contentScannerApiProvider.contentScannerApi ?: throw IllegalArgumentException()
|
||||||
|
val executeRequest = executeRequest<ScanResponse>(null) {
|
||||||
|
api.scanFile(dlBody)
|
||||||
|
}
|
||||||
|
contentScannerStore.updateScanResultForContent(
|
||||||
|
mxcUrl,
|
||||||
|
scannerUrl,
|
||||||
|
ScanState.TRUSTED.takeIf { executeRequest.clean } ?: ScanState.INFECTED,
|
||||||
|
executeRequest.info ?: ""
|
||||||
|
)
|
||||||
|
return executeRequest
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
contentScannerStore.updateStateForContent(params.mxcUrl, ScanState.UNKNOWN, scannerUrl)
|
||||||
|
throw failure.toScanFailure() ?: failure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.contentscanner.tasks
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.failure.toScanFailure
|
||||||
|
import org.matrix.android.sdk.api.session.contentscanner.ScanState
|
||||||
|
import org.matrix.android.sdk.internal.network.executeRequest
|
||||||
|
import org.matrix.android.sdk.internal.session.contentscanner.ContentScannerApiProvider
|
||||||
|
import org.matrix.android.sdk.internal.session.contentscanner.data.ContentScannerStore
|
||||||
|
import org.matrix.android.sdk.internal.session.contentscanner.model.ScanResponse
|
||||||
|
import org.matrix.android.sdk.internal.task.Task
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal interface ScanMediaTask : Task<ScanMediaTask.Params, ScanResponse> {
|
||||||
|
data class Params(
|
||||||
|
val mxcUrl: String
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class DefaultScanMediaTask @Inject constructor(
|
||||||
|
private val contentScannerApiProvider: ContentScannerApiProvider,
|
||||||
|
private val contentScannerStore: ContentScannerStore
|
||||||
|
) : ScanMediaTask {
|
||||||
|
|
||||||
|
override suspend fun execute(params: ScanMediaTask.Params): ScanResponse {
|
||||||
|
// "mxc://server.org/QNDpzLopkoQYNikJfoZCQuCXJ"
|
||||||
|
if (!params.mxcUrl.startsWith("mxc://")) {
|
||||||
|
throw IllegalAccessException("Invalid mxc url")
|
||||||
|
}
|
||||||
|
val scannerUrl = contentScannerStore.getScannerUrl()
|
||||||
|
contentScannerStore.updateStateForContent(params.mxcUrl, ScanState.IN_PROGRESS, scannerUrl)
|
||||||
|
|
||||||
|
var serverAndMediaId = params.mxcUrl.removePrefix("mxc://")
|
||||||
|
val fragmentOffset = serverAndMediaId.indexOf("#")
|
||||||
|
if (fragmentOffset >= 0) {
|
||||||
|
serverAndMediaId = serverAndMediaId.substring(0, fragmentOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
val split = serverAndMediaId.split("/")
|
||||||
|
if (split.size != 2) {
|
||||||
|
throw IllegalAccessException("Invalid mxc url")
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val scanResponse = executeRequest<ScanResponse>(null) {
|
||||||
|
val api = contentScannerApiProvider.contentScannerApi ?: throw IllegalArgumentException()
|
||||||
|
api.scanMedia(split[0], split[1])
|
||||||
|
}
|
||||||
|
contentScannerStore.updateScanResultForContent(
|
||||||
|
params.mxcUrl,
|
||||||
|
scannerUrl,
|
||||||
|
ScanState.TRUSTED.takeIf { scanResponse.clean } ?: ScanState.INFECTED,
|
||||||
|
scanResponse.info ?: ""
|
||||||
|
)
|
||||||
|
return scanResponse
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
contentScannerStore.updateStateForContent(params.mxcUrl, ScanState.UNKNOWN, scannerUrl)
|
||||||
|
throw failure.toScanFailure() ?: failure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -160,7 +160,7 @@ Formatter\.formatShortFileSize===1
|
|||||||
# android\.text\.TextUtils
|
# android\.text\.TextUtils
|
||||||
|
|
||||||
### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt
|
### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt
|
||||||
enum class===107
|
enum class===108
|
||||||
|
|
||||||
### Do not import temporary legacy classes
|
### Do not import temporary legacy classes
|
||||||
import org.matrix.android.sdk.internal.legacy.riot===3
|
import org.matrix.android.sdk.internal.legacy.riot===3
|
||||||
|
Loading…
Reference in New Issue
Block a user