mirror of
https://github.com/element-hq/element-android
synced 2024-11-24 10:25:35 +03:00
Add content scanner APIs
This commit is contained in:
parent
8b655edd34
commit
80a42d0a55
32 changed files with 1202 additions and 2 deletions
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* Copyright 2020 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 im.vector.matrix.android.sdk.internal.session.content
|
||||
|
||||
import im.vector.matrix.android.sdk.internal.session.contentscanning.ScanEncryptorUtils
|
||||
import im.vector.matrix.android.sdk.internal.session.contentscanning.model.toJson
|
||||
import org.matrix.android.sdk.api.MatrixUrls
|
||||
import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl
|
||||
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.contentscanning.ContentScannerService
|
||||
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
|
||||
import org.matrix.android.sdk.internal.network.NetworkConstants
|
||||
import org.matrix.android.sdk.internal.util.ensureTrailingSlash
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class ElementContentUrlResolver @Inject constructor(
|
||||
homeServerConnectionConfig: HomeServerConnectionConfig,
|
||||
private val scannerService: ContentScannerService
|
||||
) : ContentUrlResolver {
|
||||
|
||||
private val baseUrl = homeServerConnectionConfig.homeServerUriBase.toString().ensureTrailingSlash()
|
||||
|
||||
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? {
|
||||
return contentUrl
|
||||
// do not allow non-mxc content URLs
|
||||
?.takeIf { it.isMxcUrl() }
|
||||
?.let {
|
||||
resolve(
|
||||
contentUrl = it,
|
||||
toThumbnail = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun resolveThumbnail(contentUrl: String?, width: Int, height: Int, method: ContentUrlResolver.ThumbnailMethod): String? {
|
||||
return contentUrl
|
||||
// do not allow non-mxc content URLs
|
||||
?.takeIf { it.isMxcUrl() }
|
||||
?.let {
|
||||
resolve(
|
||||
contentUrl = it,
|
||||
toThumbnail = true,
|
||||
params = "?width=$width&height=$height&method=${method.value}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolve(contentUrl: String,
|
||||
toThumbnail: Boolean,
|
||||
params: String = ""): String? {
|
||||
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("#")
|
||||
var fragment = ""
|
||||
if (fragmentOffset >= 0) {
|
||||
fragment = serverAndMediaId.substring(fragmentOffset)
|
||||
serverAndMediaId = serverAndMediaId.substring(0, fragmentOffset)
|
||||
}
|
||||
|
||||
val resolvedUrl = if (scannerService.isScannerEnabled()) {
|
||||
scannerService.getContentScannerServer()!!.ensureTrailingSlash()
|
||||
} else {
|
||||
baseUrl
|
||||
}
|
||||
return resolvedUrl + prefix + serverAndMediaId + params + fragment
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright 2020 New Vector Ltd - All Rights Reserved
|
||||
* Unauthorized copying of this file, via any medium is strictly prohibited
|
||||
* Proprietary and confidential
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.sdk.internal.session.contentscanning
|
||||
|
||||
import im.vector.matrix.android.sdk.internal.session.contentscanning.model.DownloadBody
|
||||
import im.vector.matrix.android.sdk.internal.session.contentscanning.model.ScanResponse
|
||||
import im.vector.matrix.android.sdk.internal.session.contentscanning.model.ServerPublicKeyResponse
|
||||
import okhttp3.ResponseBody
|
||||
import org.matrix.android.sdk.internal.network.NetworkConstants
|
||||
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 ContentScanApi {
|
||||
|
||||
@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")
|
||||
fun scanFile(@Body info: DownloadBody): ScanResponse
|
||||
|
||||
@GET(NetworkConstants.URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE + "public_key")
|
||||
fun getServerPublicKey(): ServerPublicKeyResponse
|
||||
|
||||
@GET(NetworkConstants.URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE + "scan/{domain}/{mediaId}")
|
||||
fun scanMedia(@Path(value = "domain") domain: String, @Path(value = "mediaId") mediaId: String): ScanResponse
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright 2020 New Vector Ltd - All Rights Reserved
|
||||
* Unauthorized copying of this file, via any medium is strictly prohibited
|
||||
* Proprietary and confidential
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.sdk.internal.session.contentscanning
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import im.vector.matrix.android.sdk.internal.session.contentscanning.data.ContentScanningStore
|
||||
import im.vector.matrix.android.sdk.internal.session.contentscanning.db.ContentScannerRealmModule
|
||||
import im.vector.matrix.android.sdk.internal.session.contentscanning.db.RealmContentScannerStore
|
||||
import im.vector.matrix.android.sdk.internal.session.contentscanning.tasks.DefaultDownloadEncryptedTask
|
||||
import im.vector.matrix.android.sdk.internal.session.contentscanning.tasks.DefaultGetServerPublicKeyTask
|
||||
import im.vector.matrix.android.sdk.internal.session.contentscanning.tasks.DefaultScanEncryptedTask
|
||||
import im.vector.matrix.android.sdk.internal.session.contentscanning.tasks.DefaultScanMediaTask
|
||||
import im.vector.matrix.android.sdk.internal.session.contentscanning.tasks.DownloadEncryptedTask
|
||||
import im.vector.matrix.android.sdk.internal.session.contentscanning.tasks.GetServerPublicKeyTask
|
||||
import im.vector.matrix.android.sdk.internal.session.contentscanning.tasks.ScanEncryptedTask
|
||||
import im.vector.matrix.android.sdk.internal.session.contentscanning.tasks.ScanMediaTask
|
||||
import io.realm.RealmConfiguration
|
||||
import org.matrix.android.sdk.api.session.contentscanning.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 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: DefaultContentScannerService): ContentScannerService
|
||||
|
||||
@Binds
|
||||
abstract fun bindContentScannerStore(store: RealmContentScannerStore): ContentScanningStore
|
||||
|
||||
@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,15 @@
|
|||
/*
|
||||
* Copyright 2020 New Vector Ltd - All Rights Reserved
|
||||
* Unauthorized copying of this file, via any medium is strictly prohibited
|
||||
* Proprietary and confidential
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.sdk.internal.session.contentscanning
|
||||
|
||||
import org.matrix.android.sdk.internal.session.SessionScope
|
||||
import javax.inject.Inject
|
||||
|
||||
@SessionScope
|
||||
internal class ContentScanningApiProvider @Inject constructor() {
|
||||
var contentScannerApi: ContentScanApi? = null
|
||||
}
|
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* Copyright 2020 New Vector Ltd - All Rights Reserved
|
||||
* Unauthorized copying of this file, via any medium is strictly prohibited
|
||||
* Proprietary and confidential
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.sdk.internal.session.contentscanning
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import dagger.Lazy
|
||||
import im.vector.matrix.android.sdk.internal.session.contentscanning.data.ContentScanningStore
|
||||
import im.vector.matrix.android.sdk.internal.session.contentscanning.tasks.GetServerPublicKeyTask
|
||||
import im.vector.matrix.android.sdk.internal.session.contentscanning.tasks.ScanEncryptedTask
|
||||
import im.vector.matrix.android.sdk.internal.session.contentscanning.tasks.ScanMediaTask
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.OkHttpClient
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.NoOpMatrixCallback
|
||||
import org.matrix.android.sdk.api.session.contentscanning.ContentScannerService
|
||||
import org.matrix.android.sdk.api.session.contentscanning.ScanState
|
||||
import org.matrix.android.sdk.api.session.contentscanning.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.task.TaskExecutor
|
||||
import org.matrix.android.sdk.internal.task.launchToCallback
|
||||
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
||||
import org.matrix.android.sdk.internal.util.awaitCallback
|
||||
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 contentScanningApiProvider: ContentScanningApiProvider,
|
||||
private val contentScanningStore: ContentScanningStore,
|
||||
// private val sessionParams: SessionParams,
|
||||
private val getServerPublicKeyTask: GetServerPublicKeyTask,
|
||||
private val scanEncryptedTask: ScanEncryptedTask,
|
||||
private val scanMediaTask: ScanMediaTask,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers
|
||||
) : ContentScannerService {
|
||||
|
||||
// Cache public key in memory
|
||||
override var serverPublicKey: String? = null
|
||||
private set
|
||||
|
||||
override fun getContentScannerServer(): String? {
|
||||
return contentScanningStore.getScannerUrl()
|
||||
}
|
||||
|
||||
override fun getServerPublicKey(forceDownload: Boolean, callback: MatrixCallback<String?>) {
|
||||
val api = contentScanningApiProvider.contentScannerApi ?: return Unit.also {
|
||||
callback.onFailure(IllegalArgumentException("No content scanner defined"))
|
||||
}
|
||||
|
||||
if (!forceDownload && serverPublicKey != null) {
|
||||
callback.onSuccess(serverPublicKey)
|
||||
return
|
||||
}
|
||||
taskExecutor.executorScope.launchToCallback(coroutineDispatchers.io, callback) {
|
||||
getServerPublicKeyTask.execute(GetServerPublicKeyTask.Params(api)).also {
|
||||
serverPublicKey = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getScanResultForAttachment(mxcUrl: String, fileInfo: ElementToDecrypt, callback: MatrixCallback<ScanStatusInfo>) {
|
||||
taskExecutor.executorScope.launchToCallback(coroutineDispatchers.io, callback) {
|
||||
val serverPublicKey = serverPublicKey ?: awaitCallback<String?> {
|
||||
getServerPublicKey(false, it)
|
||||
}
|
||||
|
||||
val result = scanEncryptedTask.execute(ScanEncryptedTask.Params(
|
||||
mxcUrl = mxcUrl,
|
||||
publicServerKey = serverPublicKey,
|
||||
encryptedInfo = fileInfo
|
||||
))
|
||||
|
||||
ScanStatusInfo(
|
||||
state = if (result.clean) ScanState.TRUSTED else ScanState.INFECTED,
|
||||
humanReadableMessage = result.info,
|
||||
scanDateTimestamp = System.currentTimeMillis()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getScanResultForAttachment(mxcUrl: String, callback: MatrixCallback<ScanStatusInfo>) {
|
||||
taskExecutor.executorScope.launchToCallback(coroutineDispatchers.io, callback) {
|
||||
val result = scanMediaTask.execute(ScanMediaTask.Params(mxcUrl))
|
||||
|
||||
ScanStatusInfo(
|
||||
state = if (result.clean) ScanState.TRUSTED else ScanState.INFECTED,
|
||||
humanReadableMessage = result.info,
|
||||
scanDateTimestamp = System.currentTimeMillis()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setScannerUrl(url: String?) = contentScanningStore.setScannerUrl(url).also {
|
||||
if (url == null) {
|
||||
contentScanningApiProvider.contentScannerApi = null
|
||||
serverPublicKey = null
|
||||
} else {
|
||||
val api = retrofitFactory
|
||||
.create(okHttpClient, url)
|
||||
.create(ContentScanApi::class.java)
|
||||
contentScanningApiProvider.contentScannerApi = api
|
||||
|
||||
taskExecutor.executorScope.launch(coroutineDispatchers.io) {
|
||||
try {
|
||||
awaitCallback<String?> {
|
||||
getServerPublicKey(true, it)
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
Timber.e("Failed to get public server api")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun enableScanner(enabled: Boolean) = contentScanningStore.enableScanning(enabled)
|
||||
|
||||
override fun isScannerEnabled(): Boolean = contentScanningStore.isScanEnabled()
|
||||
|
||||
override fun getCachedScanResultForFile(mxcUrl: String): ScanStatusInfo? {
|
||||
return contentScanningStore.getScanResult(mxcUrl)
|
||||
}
|
||||
|
||||
override fun getLiveStatusForFile(mxcUrl: String, fetchIfNeeded: Boolean): LiveData<Optional<ScanStatusInfo>> {
|
||||
val data = contentScanningStore.getLiveScanResult(mxcUrl)
|
||||
if (fetchIfNeeded && !contentScanningStore.isScanResultKnownOrInProgress(mxcUrl, getContentScannerServer())) {
|
||||
getScanResultForAttachment(mxcUrl, NoOpMatrixCallback())
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
override fun getLiveStatusForEncryptedFile(mxcUrl: String, fileInfo: ElementToDecrypt, fetchIfNeeded: Boolean): LiveData<Optional<ScanStatusInfo>> {
|
||||
val data = contentScanningStore.getLiveScanResult(mxcUrl)
|
||||
if (fetchIfNeeded && !contentScanningStore.isScanResultKnownOrInProgress(mxcUrl, getContentScannerServer())) {
|
||||
getScanResultForAttachment(mxcUrl, fileInfo, NoOpMatrixCallback())
|
||||
}
|
||||
return data
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright 2020 New Vector Ltd - All Rights Reserved
|
||||
* Unauthorized copying of this file, via any medium is strictly prohibited
|
||||
* Proprietary and confidential
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.sdk.internal.session.contentscanning
|
||||
|
||||
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 im.vector.matrix.android.sdk.internal.session.contentscanning.model.DownloadBody
|
||||
import im.vector.matrix.android.sdk.internal.session.contentscanning.model.EncryptedBody
|
||||
import im.vector.matrix.android.sdk.internal.session.contentscanning.model.toCanonicalJson
|
||||
|
||||
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,30 @@
|
|||
/*
|
||||
* Copyright 2020 New Vector Ltd - All Rights Reserved
|
||||
* Unauthorized copying of this file, via any medium is strictly prohibited
|
||||
* Proprietary and confidential
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.sdk.internal.session.contentscanning.data
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import org.matrix.android.sdk.api.session.contentscanning.ScanState
|
||||
import org.matrix.android.sdk.api.session.contentscanning.ScanStatusInfo
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
|
||||
internal interface ContentScanningStore {
|
||||
|
||||
fun getScannerUrl(): String?
|
||||
|
||||
fun setScannerUrl(url: String?)
|
||||
|
||||
fun enableScanning(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,45 @@
|
|||
/*
|
||||
* Copyright 2020 New Vector Ltd - All Rights Reserved
|
||||
* Unauthorized copying of this file, via any medium is strictly prohibited
|
||||
* Proprietary and confidential
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.sdk.internal.session.contentscanning.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.contentscanning.ScanState
|
||||
import org.matrix.android.sdk.api.session.contentscanning.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,31 @@
|
|||
/*
|
||||
* Copyright 2020 New Vector Ltd - All Rights Reserved
|
||||
* Unauthorized copying of this file, via any medium is strictly prohibited
|
||||
* Proprietary and confidential
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.sdk.internal.session.contentscanning.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,17 @@
|
|||
/*
|
||||
* Copyright 2020 New Vector Ltd - All Rights Reserved
|
||||
* Unauthorized copying of this file, via any medium is strictly prohibited
|
||||
* Proprietary and confidential
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.sdk.internal.session.contentscanning.db
|
||||
|
||||
import io.realm.RealmObject
|
||||
|
||||
internal open class ContentScannerInfoEntity(
|
||||
var serverUrl: String? = null,
|
||||
var enabled: Boolean? = null
|
||||
) : RealmObject() {
|
||||
|
||||
companion object
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright 2020 New Vector Ltd - All Rights Reserved
|
||||
* Unauthorized copying of this file, via any medium is strictly prohibited
|
||||
* Proprietary and confidential
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.sdk.internal.session.contentscanning.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,132 @@
|
|||
/*
|
||||
* Copyright 2020 New Vector Ltd - All Rights Reserved
|
||||
* Unauthorized copying of this file, via any medium is strictly prohibited
|
||||
* Proprietary and confidential
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.sdk.internal.session.contentscanning.db
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.sdk.internal.session.contentscanning.data.ContentScanningStore
|
||||
import org.matrix.android.sdk.api.session.contentscanning.ScanState
|
||||
import org.matrix.android.sdk.api.session.contentscanning.ScanStatusInfo
|
||||
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.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.util.isValidUrl
|
||||
import javax.inject.Inject
|
||||
|
||||
@SessionScope
|
||||
internal class RealmContentScannerStore @Inject constructor(
|
||||
@ContentScannerDatabase
|
||||
private val realmConfiguration: RealmConfiguration
|
||||
) : ContentScanningStore {
|
||||
|
||||
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 enableScanning(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,30 @@
|
|||
/*
|
||||
* Copyright 2020 New Vector Ltd - All Rights Reserved
|
||||
* Unauthorized copying of this file, via any medium is strictly prohibited
|
||||
* Proprietary and confidential
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.sdk.internal.session.contentscanning.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)
|
||||
data class DownloadBody(
|
||||
@Json(name = "file") val file: EncryptedFileInfo? = null,
|
||||
@Json(name = "encrypted_body") val encryptedBody: EncryptedBody? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class EncryptedBody(
|
||||
@Json(name = "ciphertext") val cipherText: String,
|
||||
@Json(name = "mac") val mac: String,
|
||||
@Json(name = "ephemeral") val ephemeral: String
|
||||
)
|
||||
|
||||
fun DownloadBody.toJson(): String = MoshiProvider.providesMoshi().adapter(DownloadBody::class.java).toJson(this)
|
||||
|
||||
fun DownloadBody.toCanonicalJson() = JsonCanonicalizer.getCanonicalJson(DownloadBody::class.java, this)
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright 2020 New Vector Ltd - All Rights Reserved
|
||||
* Unauthorized copying of this file, via any medium is strictly prohibited
|
||||
* Proprietary and confidential
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.sdk.internal.session.contentscanning.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)
|
||||
data class ScanResponse(
|
||||
@Json(name = "clean") val clean: Boolean,
|
||||
/** Human-readable information about the result. */
|
||||
@Json(name = "info") val info: String?
|
||||
)
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright 2020 New Vector Ltd - All Rights Reserved
|
||||
* Unauthorized copying of this file, via any medium is strictly prohibited
|
||||
* Proprietary and confidential
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.sdk.internal.session.contentscanning.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ServerPublicKeyResponse(
|
||||
@Json(name = "public_key")
|
||||
val publicKey : String?
|
||||
)
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright 2020 New Vector Ltd - All Rights Reserved
|
||||
* Unauthorized copying of this file, via any medium is strictly prohibited
|
||||
* Proprietary and confidential
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.sdk.internal.session.contentscanning.tasks
|
||||
|
||||
import im.vector.matrix.android.sdk.internal.session.contentscanning.ContentScanningApiProvider
|
||||
import im.vector.matrix.android.sdk.internal.session.contentscanning.ScanEncryptorUtils
|
||||
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.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 contentScanningApiProvider: ContentScanningApiProvider
|
||||
) : DownloadEncryptedTask {
|
||||
|
||||
override suspend fun execute(params: DownloadEncryptedTask.Params): ResponseBody {
|
||||
val dlBody = ScanEncryptorUtils.getDownloadBodyAndEncryptIfNeeded(
|
||||
params.publicServerKey,
|
||||
params.mxcUrl,
|
||||
params.encryptedInfo
|
||||
)
|
||||
|
||||
val api = contentScanningApiProvider.contentScannerApi ?: throw IllegalArgumentException()
|
||||
return executeRequest(null) {
|
||||
api.downloadEncrypted(dlBody)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright 2020 New Vector Ltd - All Rights Reserved
|
||||
* Unauthorized copying of this file, via any medium is strictly prohibited
|
||||
* Proprietary and confidential
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.sdk.internal.session.contentscanning.tasks
|
||||
|
||||
import im.vector.matrix.android.sdk.internal.session.contentscanning.ContentScanApi
|
||||
import im.vector.matrix.android.sdk.internal.session.contentscanning.model.ServerPublicKeyResponse
|
||||
import org.matrix.android.sdk.internal.network.executeRequest
|
||||
import org.matrix.android.sdk.internal.task.Task
|
||||
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface GetServerPublicKeyTask : Task<GetServerPublicKeyTask.Params, String?> {
|
||||
data class Params(
|
||||
val contentScanApi: ContentScanApi
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultGetServerPublicKeyTask @Inject constructor() : GetServerPublicKeyTask {
|
||||
|
||||
override suspend fun execute(params: GetServerPublicKeyTask.Params): String? {
|
||||
return executeRequest<ServerPublicKeyResponse>(null) {
|
||||
params.contentScanApi.getServerPublicKey()
|
||||
}.publicKey
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright 2020 New Vector Ltd - All Rights Reserved
|
||||
* Unauthorized copying of this file, via any medium is strictly prohibited
|
||||
* Proprietary and confidential
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.sdk.internal.session.contentscanning.tasks
|
||||
|
||||
import im.vector.matrix.android.sdk.internal.session.contentscanning.ContentScanningApiProvider
|
||||
import im.vector.matrix.android.sdk.internal.session.contentscanning.ScanEncryptorUtils
|
||||
import im.vector.matrix.android.sdk.internal.session.contentscanning.data.ContentScanningStore
|
||||
import im.vector.matrix.android.sdk.internal.session.contentscanning.model.ScanResponse
|
||||
import org.matrix.android.sdk.api.failure.toScanFailure
|
||||
import org.matrix.android.sdk.api.session.contentscanning.ScanState
|
||||
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
|
||||
import org.matrix.android.sdk.internal.network.executeRequest
|
||||
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 contentScanningApiProvider: ContentScanningApiProvider,
|
||||
private val contentScanningStore: ContentScanningStore
|
||||
) : 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 = contentScanningStore.getScannerUrl()
|
||||
contentScanningStore.updateStateForContent(params.mxcUrl, ScanState.IN_PROGRESS, scannerUrl)
|
||||
|
||||
try {
|
||||
val api = contentScanningApiProvider.contentScannerApi ?: throw IllegalArgumentException()
|
||||
val executeRequest = executeRequest<ScanResponse>(null) {
|
||||
api.scanFile(dlBody)
|
||||
}
|
||||
contentScanningStore.updateScanResultForContent(
|
||||
mxcUrl,
|
||||
scannerUrl,
|
||||
ScanState.TRUSTED.takeIf { executeRequest.clean } ?: ScanState.INFECTED,
|
||||
executeRequest.info ?: ""
|
||||
)
|
||||
return executeRequest
|
||||
} catch (failure: Throwable) {
|
||||
contentScanningStore.updateStateForContent(params.mxcUrl, ScanState.UNKNOWN, scannerUrl)
|
||||
throw failure.toScanFailure() ?: failure
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright 2020 New Vector Ltd - All Rights Reserved
|
||||
* Unauthorized copying of this file, via any medium is strictly prohibited
|
||||
* Proprietary and confidential
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.sdk.internal.session.contentscanning.tasks
|
||||
|
||||
import im.vector.matrix.android.sdk.internal.session.contentscanning.ContentScanningApiProvider
|
||||
import im.vector.matrix.android.sdk.internal.session.contentscanning.data.ContentScanningStore
|
||||
import im.vector.matrix.android.sdk.internal.session.contentscanning.model.ScanResponse
|
||||
import org.matrix.android.sdk.api.failure.toScanFailure
|
||||
import org.matrix.android.sdk.api.session.contentscanning.ScanState
|
||||
import org.matrix.android.sdk.internal.network.executeRequest
|
||||
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 contentScanningApiProvider: ContentScanningApiProvider,
|
||||
private val contentScanningStore: ContentScanningStore
|
||||
) : 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 = contentScanningStore.getScannerUrl()
|
||||
contentScanningStore.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 = contentScanningApiProvider.contentScannerApi ?: throw IllegalArgumentException()
|
||||
api.scanMedia(split[0], split[1])
|
||||
}
|
||||
contentScanningStore.updateScanResultForContent(
|
||||
params.mxcUrl,
|
||||
scannerUrl,
|
||||
ScanState.TRUSTED.takeIf { scanResponse.clean } ?: ScanState.INFECTED,
|
||||
scanResponse.info ?: ""
|
||||
)
|
||||
return scanResponse
|
||||
} catch (failure: Throwable) {
|
||||
contentScanningStore.updateStateForContent(params.mxcUrl, ScanState.UNKNOWN, scannerUrl)
|
||||
throw failure.toScanFailure() ?: failure
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.contentscanning.ContentScanError
|
||||
import org.matrix.android.sdk.api.session.contentscanning.ScanFailure
|
||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
import java.io.IOException
|
||||
import javax.net.ssl.HttpsURLConnection
|
||||
|
@ -100,3 +102,19 @@ fun Throwable.isRegistrationAvailabilityError(): Boolean {
|
|||
error.code == MatrixError.M_INVALID_USERNAME ||
|
||||
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(ContentScanError::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.content.ContentUploadStateTracker
|
||||
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
|
||||
import org.matrix.android.sdk.api.session.contentscanning.ContentScannerService
|
||||
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.file.ContentDownloadStateTracker
|
||||
|
@ -192,6 +193,11 @@ interface Session :
|
|||
*/
|
||||
fun cryptoService(): CryptoService
|
||||
|
||||
/**
|
||||
* Returns the ContentScanningService associated with the session
|
||||
*/
|
||||
fun contentScanningService(): ContentScannerService
|
||||
|
||||
/**
|
||||
* Returns the identity service associated with the session
|
||||
*/
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
|
||||
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.
|
||||
*/
|
||||
|
@ -39,6 +41,15 @@ interface ContentUrlResolver {
|
|||
*/
|
||||
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.
|
||||
*
|
||||
|
@ -49,4 +60,9 @@ interface ContentUrlResolver {
|
|||
* @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?
|
||||
|
||||
sealed class ResolvedMethod {
|
||||
data class GET(val url: String) : ResolvedMethod()
|
||||
data class POST(val url:String, val jsonBody: String): ResolvedMethod()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright 2020 New Vector Ltd - All Rights Reserved
|
||||
* Unauthorized copying of this file, via any medium is strictly prohibited
|
||||
* Proprietary and confidential
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.api.session.contentscanning
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ContentScanError(
|
||||
@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: ContentScanError, 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,35 @@
|
|||
/*
|
||||
* Copyright 2020 New Vector Ltd - All Rights Reserved
|
||||
* Unauthorized copying of this file, via any medium is strictly prohibited
|
||||
* Proprietary and confidential
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.api.session.contentscanning
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
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?
|
||||
/**
|
||||
* Get the current public curve25519 key that the AV server is advertising.
|
||||
* @param callback on success callback containing the server public key
|
||||
*/
|
||||
fun getServerPublicKey(forceDownload: Boolean = false, callback: MatrixCallback<String?>)
|
||||
|
||||
fun getScanResultForAttachment(mxcUrl: String, callback: MatrixCallback<ScanStatusInfo>)
|
||||
fun getScanResultForAttachment(mxcUrl: String, fileInfo: ElementToDecrypt, callback: MatrixCallback<ScanStatusInfo>)
|
||||
|
||||
fun setScannerUrl(url: String?)
|
||||
|
||||
fun enableScanner(enabled: Boolean)
|
||||
fun isScannerEnabled(): Boolean
|
||||
fun getLiveStatusForFile(mxcUrl: String, fetchIfNeeded: Boolean = true): LiveData<Optional<ScanStatusInfo>>
|
||||
fun getLiveStatusForEncryptedFile(mxcUrl: String, fileInfo: ElementToDecrypt, fetchIfNeeded: Boolean = true): LiveData<Optional<ScanStatusInfo>>
|
||||
fun getCachedScanResultForFile(mxcUrl: String): ScanStatusInfo?
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright (c) 2021 New Vector Ltd
|
||||
*
|
||||
* 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.contentscanning
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.session.contentscanning.ContentScannerService
|
||||
import org.matrix.android.sdk.api.session.contentscanning.ScanStatusInfo
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
|
||||
|
||||
/**
|
||||
* Created to by-pass ProfileTask execution in LoginWizard.
|
||||
*/
|
||||
class DisabledContentScannerService : ContentScannerService {
|
||||
|
||||
override val serverPublicKey: String?
|
||||
get() = null
|
||||
|
||||
override fun getContentScannerServer(): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getServerPublicKey(forceDownload: Boolean, callback: MatrixCallback<String?>) {
|
||||
}
|
||||
|
||||
override fun getScanResultForAttachment(mxcUrl: String, callback: MatrixCallback<ScanStatusInfo>) {
|
||||
}
|
||||
|
||||
override fun getScanResultForAttachment(mxcUrl: String, fileInfo: ElementToDecrypt, callback: MatrixCallback<ScanStatusInfo>) {
|
||||
}
|
||||
|
||||
override fun setScannerUrl(url: String?) {
|
||||
}
|
||||
|
||||
override fun enableScanner(enabled: Boolean) {
|
||||
}
|
||||
|
||||
override fun isScannerEnabled(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun getLiveStatusForFile(mxcUrl: String, fetchIfNeeded: Boolean): LiveData<Optional<ScanStatusInfo>> {
|
||||
return MutableLiveData()
|
||||
}
|
||||
|
||||
override fun getLiveStatusForEncryptedFile(mxcUrl: String, fileInfo: ElementToDecrypt, fetchIfNeeded: Boolean): LiveData<Optional<ScanStatusInfo>> {
|
||||
return MutableLiveData()
|
||||
}
|
||||
|
||||
override fun getCachedScanResultForFile(mxcUrl: String): ScanStatusInfo? {
|
||||
return null
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright 2020 New Vector Ltd - All Rights Reserved
|
||||
* Unauthorized copying of this file, via any medium is strictly prohibited
|
||||
* Proprietary and confidential
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.api.session.contentscanning
|
||||
|
||||
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.network.executeRequest
|
||||
import org.matrix.android.sdk.internal.session.content.DefaultContentUrlResolver
|
||||
import org.matrix.android.sdk.internal.session.contentscanning.DisabledContentScannerService
|
||||
|
||||
internal class DefaultLoginWizard(
|
||||
private val authAPI: AuthAPI,
|
||||
|
@ -44,7 +45,7 @@ internal class DefaultLoginWizard(
|
|||
|
||||
private val getProfileTask: GetProfileTask = DefaultGetProfileTask(
|
||||
authAPI,
|
||||
DefaultContentUrlResolver(pendingSessionData.homeServerConnectionConfig)
|
||||
DefaultContentUrlResolver(pendingSessionData.homeServerConnectionConfig, DisabledContentScannerService())
|
||||
)
|
||||
|
||||
override suspend fun getProfileInfo(matrixId: String): LoginProfileInfo {
|
||||
|
|
|
@ -37,3 +37,7 @@ internal annotation class CryptoDatabase
|
|||
@Qualifier
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
internal annotation class IdentityDatabase
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
internal annotation class ContentScannerDatabase
|
||||
|
|
|
@ -38,6 +38,9 @@ internal object NetworkConstants {
|
|||
// Integration
|
||||
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
|
||||
const val URI_FEDERATION_PATH = "_matrix/federation/v1/"
|
||||
}
|
||||
|
|
|
@ -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.content.ContentUploadStateTracker
|
||||
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
|
||||
import org.matrix.android.sdk.api.session.contentscanning.ContentScannerService
|
||||
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.file.ContentDownloadStateTracker
|
||||
|
@ -124,6 +125,7 @@ internal class DefaultSession @Inject constructor(
|
|||
private val _sharedSecretStorageService: Lazy<SharedSecretStorageService>,
|
||||
private val accountService: Lazy<AccountService>,
|
||||
private val eventService: Lazy<EventService>,
|
||||
private val contentScannerService: Lazy<ContentScannerService>,
|
||||
private val identityService: IdentityService,
|
||||
private val integrationManagerService: IntegrationManagerService,
|
||||
private val thirdPartyService: Lazy<ThirdPartyService>,
|
||||
|
@ -275,6 +277,8 @@ internal class DefaultSession @Inject constructor(
|
|||
|
||||
override fun cryptoService(): CryptoService = cryptoService.get()
|
||||
|
||||
override fun contentScanningService(): ContentScannerService = contentScannerService.get()
|
||||
|
||||
override fun identityService() = identityService
|
||||
|
||||
override fun fileService(): FileService = defaultFileService.get()
|
||||
|
|
|
@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session
|
|||
|
||||
import dagger.BindsInstance
|
||||
import dagger.Component
|
||||
import im.vector.matrix.android.sdk.internal.session.contentscanning.ContentScannerModule
|
||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||
import org.matrix.android.sdk.api.auth.data.SessionParams
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
|
@ -94,6 +95,7 @@ import org.matrix.android.sdk.internal.util.system.SystemModule
|
|||
AccountModule::class,
|
||||
FederationModule::class,
|
||||
CallModule::class,
|
||||
ContentScannerModule::class,
|
||||
SearchModule::class,
|
||||
ThirdPartyModule::class,
|
||||
SpaceModule::class,
|
||||
|
|
|
@ -20,11 +20,16 @@ import org.matrix.android.sdk.api.MatrixUrls
|
|||
import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl
|
||||
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.contentscanning.ContentScannerService
|
||||
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
|
||||
import org.matrix.android.sdk.internal.network.NetworkConstants
|
||||
import org.matrix.android.sdk.internal.util.ensureTrailingSlash
|
||||
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()
|
||||
|
||||
|
@ -55,6 +60,10 @@ internal class DefaultContentUrlResolver @Inject constructor(homeServerConnectio
|
|||
}
|
||||
}
|
||||
|
||||
override fun resolveForDownload(contentUrl: String?, elementToDecrypt: ElementToDecrypt?): ContentUrlResolver.ResolvedMethod? {
|
||||
return resolveFullSize(contentUrl)?.let { ContentUrlResolver.ResolvedMethod.GET(it) }
|
||||
}
|
||||
|
||||
private fun resolve(contentUrl: String,
|
||||
prefix: String,
|
||||
params: String = ""): String? {
|
||||
|
|
Loading…
Reference in a new issue