Skip to content

Commit

Permalink
Add content scanner service (#4392)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
bmarty and yostyle authored Nov 17, 2021
2 parents 36c312b + 0fada97 commit 855b672
Show file tree
Hide file tree
Showing 34 changed files with 1,321 additions and 12 deletions.
2 changes: 2 additions & 0 deletions changelog.d/4392.removal
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add content scanner API from MSC1453
API documentation : https://github.com/matrix-org/matrix-content-scanner#api
Original file line number Diff line number Diff line change
Expand Up @@ -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.contentscanner.ContentScannerError
import org.matrix.android.sdk.api.session.contentscanner.ScanFailure
import org.matrix.android.sdk.internal.di.MoshiProvider
import java.io.IOException
import javax.net.ssl.HttpsURLConnection
Expand Down Expand Up @@ -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(ContentScannerError::class.java)
.fromJson(errorBody)
}
?.let { ScanFailure(it, httpCode, this) }
} else {
null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.contentscanner.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
Expand Down Expand Up @@ -192,6 +193,11 @@ interface Session :
*/
fun cryptoService(): CryptoService

/**
* Returns the ContentScannerService associated with the session
*/
fun contentScannerService(): ContentScannerService

/**
* Returns the identity service associated with the session
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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.
*
Expand All @@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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?
)
Original file line number Diff line number Diff line change
Expand Up @@ -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.contentscanner.DisabledContentScannerService

internal class DefaultLoginWizard(
private val authAPI: AuthAPI,
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,7 @@ internal annotation class CryptoDatabase
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
internal annotation class IdentityDatabase

@Qualifier
@Retention(AnnotationRetention.RUNTIME)
internal annotation class ContentScannerDatabase
Original file line number Diff line number Diff line change
Expand Up @@ -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/"
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ import androidx.core.content.FileProvider
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.completeWith
import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
Expand Down Expand Up @@ -118,12 +120,24 @@ internal class DefaultFileService @Inject constructor(
val cachedFiles = getFiles(url, fileName, mimeType, elementToDecrypt != null)

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 = when (resolvedUrl) {
is ContentUrlResolver.ResolvedMethod.GET -> {
Request.Builder()
.url(resolvedUrl.url)
.header(DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER, url)
.build()
}

val request = Request.Builder()
.url(resolvedUrl)
.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 {
okHttpClient.newCall(request).execute()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.contentscanner.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
Expand Down Expand Up @@ -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>,
Expand Down Expand Up @@ -275,6 +277,8 @@ internal class DefaultSession @Inject constructor(

override fun cryptoService(): CryptoService = cryptoService.get()

override fun contentScannerService(): ContentScannerService = contentScannerService.get()

override fun identityService() = identityService

override fun fileService(): FileService = defaultFileService.get()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.content.ContentModule
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.group.GetGroupDataWorker
import org.matrix.android.sdk.internal.session.group.GroupModule
Expand Down Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 855b672

Please sign in to comment.