Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add content scanner service #4392

Merged
merged 7 commits into from
Nov 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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