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

Abstract the toolkit over publication source #353

Merged
merged 37 commits into from
Jul 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
8fc2516
WIP
qnga Apr 20, 2023
30b1ebe
WIP 2
qnga May 11, 2023
0546f5f
WIP 3
qnga May 12, 2023
210130f
Working version
qnga May 24, 2023
86d516c
Remove the HTTP server
qnga May 25, 2023
5c0d15a
New working version
qnga Jun 5, 2023
e104f4f
Various changes
qnga Jun 7, 2023
a40ed4e
Turn AssetAnalyzer into AssetRetriever
qnga Jun 11, 2023
342ccd2
Improve resource package
qnga Jun 11, 2023
beed329
Various fixes
qnga Jun 12, 2023
6011615
Refactor Url and add ContentResourceFactory
qnga Jun 15, 2023
05f4f45
Introduce ProtectionRetriever
qnga Jun 20, 2023
3f69838
Fix HttpResource
qnga Jun 22, 2023
579a51e
Various fixes including support for manifests
qnga Jun 22, 2023
b8e7edd
Add support for adding files from shared storage
qnga Jun 22, 2023
b449a49
Various fixes
qnga Jun 23, 2023
99f1532
Various changes
qnga Jun 24, 2023
631d2eb
Improve error handling
qnga Jun 27, 2023
4b58bb1
Rework error handling
qnga Jul 2, 2023
05b7013
Fixes in LcpContentProtection
qnga Jul 3, 2023
9e9fd33
Various fixes
qnga Jul 4, 2023
63cf6a0
Cosmetic Change
qnga Jul 6, 2023
7ee4d88
Merge branch 'v3' of github.com:readium/kotlin-toolkit into refactor-…
qnga Jul 6, 2023
fc614e7
Various changes
qnga Jul 6, 2023
1cc6d7d
Various changes
qnga Jul 6, 2023
a9278ba
Small fix
qnga Jul 6, 2023
da941f0
Merge branch 'v3' of github.com:readium/kotlin-toolkit into refactor-…
qnga Jul 8, 2023
b5de53a
Various changes in shared
qnga Jul 9, 2023
e25e022
Cosmetic changes
qnga Jul 9, 2023
4e40f83
Fix ZAB
qnga Jul 9, 2023
456c7e9
Various fixes
qnga Jul 9, 2023
9d670bf
Various changes
qnga Jul 10, 2023
030d96d
Various changes
qnga Jul 14, 2023
34715dd
Rename SimpleError to MessageError
qnga Jul 14, 2023
a7d8099
Refactor ContentProtection.Scheme and HTTP-related MediaType extensions
qnga Jul 16, 2023
c704a70
Various changes
qnga Jul 17, 2023
7255b2a
Merge branch 'v3' of github.com:readium/kotlin-toolkit into refactor-…
qnga Jul 17, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ import kotlin.reflect.KClass
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.readium.r2.shared.PdfSupport
import org.readium.r2.shared.error.getOrThrow
import org.readium.r2.shared.extensions.md5
import org.readium.r2.shared.extensions.tryOrNull
import org.readium.r2.shared.fetcher.Resource
import org.readium.r2.shared.fetcher.Fetcher
import org.readium.r2.shared.util.pdf.PdfDocument
import org.readium.r2.shared.util.pdf.PdfDocumentFactory
import org.readium.r2.shared.util.use
Expand Down Expand Up @@ -89,19 +90,19 @@ class PdfiumDocumentFactory(context: Context) : PdfDocumentFactory<PdfiumDocumen
override suspend fun open(file: File, password: String?): PdfiumDocument =
core.fromFile(file, password)

override suspend fun open(resource: Resource, password: String?): PdfiumDocument {
override suspend fun open(resource: Fetcher.Resource, password: String?): PdfiumDocument {
// First try to open the resource as a file on the FS for performance improvement, as
// PDFium requires the whole PDF document to be loaded in memory when using raw bytes.
return resource.openAsFile(password)
?: resource.openBytes(password)
}

private suspend fun Resource.openAsFile(password: String?): PdfiumDocument? =
private suspend fun Fetcher.Resource.openAsFile(password: String?): PdfiumDocument? =
file?.let {
tryOrNull { open(it, password) }
}

private suspend fun Resource.openBytes(password: String?): PdfiumDocument =
private suspend fun Fetcher.Resource.openBytes(password: String?): PdfiumDocument =
use {
core.fromBytes(read().getOrThrow(), password)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ import org.readium.r2.navigator.preferences.Axis
import org.readium.r2.navigator.preferences.Fit
import org.readium.r2.navigator.preferences.ReadingProgression
import org.readium.r2.shared.ExperimentalReadiumApi
import org.readium.r2.shared.fetcher.Resource
import org.readium.r2.shared.publication.Link
import org.readium.r2.shared.publication.Publication
import org.readium.r2.shared.resource.Resource
import timber.log.Timber

@ExperimentalReadiumApi
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import java.io.File
import kotlin.reflect.KClass
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.readium.r2.shared.fetcher.Resource
import org.readium.r2.shared.fetcher.Fetcher
import org.readium.r2.shared.publication.ReadingProgression
import org.readium.r2.shared.util.pdf.PdfDocument
import org.readium.r2.shared.util.pdf.PdfDocumentFactory
Expand All @@ -33,7 +33,7 @@ class PsPdfKitDocumentFactory(context: Context) : PdfDocumentFactory<PsPdfKitDoc
override suspend fun open(file: File, password: String?): PsPdfKitDocument =
open(context, DocumentSource(file.toUri(), password))

override suspend fun open(resource: Resource, password: String?): PsPdfKitDocument =
override suspend fun open(resource: Fetcher.Resource, password: String?): PsPdfKitDocument =
open(context, DocumentSource(ResourceDataProvider(resource), password))

private suspend fun open(context: Context, documentSource: DocumentSource): PsPdfKitDocument =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ package org.readium.adapters.pspdfkit.document
import com.pspdfkit.document.providers.DataProvider
import java.util.*
import kotlinx.coroutines.runBlocking
import org.readium.r2.shared.fetcher.Resource
import org.readium.r2.shared.error.getOrElse
import org.readium.r2.shared.fetcher.Fetcher
import org.readium.r2.shared.fetcher.synchronized
import org.readium.r2.shared.util.getOrElse
import org.readium.r2.shared.resource.Resource
import org.readium.r2.shared.util.isLazyInitialized
import timber.log.Timber

class ResourceDataProvider(
resource: Resource,
resource: Fetcher.Resource,
private val onResourceError: (Resource.Exception) -> Unit = { Timber.e(it) }
) : DataProvider {

Expand Down
195 changes: 144 additions & 51 deletions readium/lcp/src/main/java/org/readium/r2/lcp/LcpContentProtection.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,94 +7,187 @@
package org.readium.r2.lcp

import org.readium.r2.lcp.auth.LcpPassphraseAuthentication
import org.readium.r2.lcp.service.LcpLicensedAsset
import org.readium.r2.lcp.license.model.LicenseDocument
import org.readium.r2.shared.asset.Asset
import org.readium.r2.shared.error.ThrowableError
import org.readium.r2.shared.error.Try
import org.readium.r2.shared.error.getOrElse
import org.readium.r2.shared.fetcher.ContainerFetcher
import org.readium.r2.shared.fetcher.TransformingFetcher
import org.readium.r2.shared.publication.ContentProtection
import org.readium.r2.shared.publication.Publication
import org.readium.r2.shared.publication.asset.FileAsset
import org.readium.r2.shared.publication.asset.PublicationAsset
import org.readium.r2.shared.publication.asset.RemoteAsset
import org.readium.r2.shared.publication.protection.ContentProtection
import org.readium.r2.shared.publication.services.contentProtectionServiceFactory
import org.readium.r2.shared.util.Try
import org.readium.r2.shared.resource.ArchiveFactory
import org.readium.r2.shared.resource.Resource
import org.readium.r2.shared.resource.ResourceFactory
import org.readium.r2.shared.util.Url
import org.readium.r2.shared.util.mediatype.MediaTypeRetriever

internal class LcpContentProtection(
private val lcpService: LcpService,
private val authentication: LcpAuthenticating
private val authentication: LcpAuthenticating,
private val mediaTypeRetriever: MediaTypeRetriever,
private val resourceFactory: ResourceFactory,
private val archiveFactory: ArchiveFactory
) : ContentProtection {

override val scheme: ContentProtection.Scheme =
ContentProtection.Scheme.Lcp

override suspend fun supports(
asset: Asset
): Boolean =
lcpService.isLcpProtected(asset)

override suspend fun open(
asset: PublicationAsset,
asset: Asset,
credentials: String?,
allowUserInteraction: Boolean,
sender: Any?
): Try<ContentProtection.ProtectedAsset, Publication.OpeningException>? {
): Try<ContentProtection.Asset, Publication.OpeningException> {
return when (asset) {
is Asset.Container -> openPublication(asset, credentials, allowUserInteraction, sender)
is Asset.Resource -> openLicense(asset, credentials, allowUserInteraction, sender)
}
mickael-menu marked this conversation as resolved.
Show resolved Hide resolved
}

private suspend fun openPublication(
asset: Asset.Container,
credentials: String?,
allowUserInteraction: Boolean,
sender: Any?
): Try<ContentProtection.Asset, Publication.OpeningException> {
val license = retrieveLicense(asset, credentials, allowUserInteraction, sender)
?: return null
return createProtectedAsset(asset, license)
return createResultAsset(asset, license)
}

/* Returns null if the publication is not protected by LCP. */
private suspend fun retrieveLicense(
asset: PublicationAsset,
asset: Asset,
credentials: String?,
allowUserInteraction: Boolean,
sender: Any?
): Try<LcpLicense, LcpException>? {

): Try<LcpLicense, LcpException> {
val authentication = credentials
?.let { LcpPassphraseAuthentication(it, fallback = this.authentication) }
?: this.authentication

val license = when (asset) {
is FileAsset ->
lcpService.retrieveLicense(asset.file, authentication, allowUserInteraction, sender)
is RemoteAsset ->
lcpService.retrieveLicense(asset.fetcher, asset.mediaType, authentication, allowUserInteraction, sender)
is LcpLicensedAsset ->
asset.license
?.let { Try.success(it) }
?: lcpService.retrieveLicense(asset.licenseFile, authentication, allowUserInteraction, sender)
else ->
null
}
val file = (asset as? Asset.Resource)?.resource?.file
?: (asset as? Asset.Container)?.container?.file

return license?.takeUnless { result ->
result is Try.Failure<*, *> && result.exception is LcpException.Container
}
return file
// This is less restrictive with regard to network availability.
?.let { lcpService.retrieveLicense(it, asset.mediaType, authentication, allowUserInteraction, sender) }
?: lcpService.retrieveLicense(asset, authentication, allowUserInteraction, sender)
}

private fun createProtectedAsset(
originalAsset: PublicationAsset,
private fun createResultAsset(
asset: Asset.Container,
license: Try<LcpLicense, LcpException>,
): Try<ContentProtection.ProtectedAsset, Publication.OpeningException> {
): Try<ContentProtection.Asset, Publication.OpeningException> {
val serviceFactory = LcpContentProtectionService
.createFactory(license.getOrNull(), license.exceptionOrNull())
.createFactory(license.getOrNull(), license.failureOrNull())

val newFetcher = TransformingFetcher(
originalAsset.fetcher,
val fetcher = TransformingFetcher(
ContainerFetcher(asset.container, mediaTypeRetriever),
LcpDecryptor(license.getOrNull())::transform
)

val newAsset = when (originalAsset) {
is FileAsset -> {
originalAsset.copy(fetcher = newFetcher)
}
is RemoteAsset -> {
originalAsset.copy(fetcher = newFetcher)
}
is LcpLicensedAsset -> {
originalAsset.copy(fetcher = newFetcher)
}
else -> throw IllegalStateException()
}

val protectedFile = ContentProtection.ProtectedAsset(
asset = newAsset,
val protectedFile = ContentProtection.Asset(
name = asset.name,
mediaType = asset.mediaType,
fetcher = fetcher,
onCreatePublication = {
servicesBuilder.contentProtectionServiceFactory = serviceFactory
}
)

return Try.success(protectedFile)
}

private suspend fun openLicense(
licenseAsset: Asset.Resource,
credentials: String?,
allowUserInteraction: Boolean,
sender: Any?
): Try<ContentProtection.Asset, Publication.OpeningException> {
val license = retrieveLicense(licenseAsset, credentials, allowUserInteraction, sender)

val licenseDoc = license.getOrNull()?.license
?: licenseAsset.resource.read()
.map {
try {
LicenseDocument(it)
} catch (e: Exception) {
return Try.failure(
Publication.OpeningException.ParsingFailed(
ThrowableError(e)
)
)
}
}
.getOrElse {
return Try.failure(
it.wrap()
)
}

val link = checkNotNull(licenseDoc.link(LicenseDocument.Rel.publication))
val url = Url(link.url.toString())
?: return Try.failure(
Publication.OpeningException.ParsingFailed(
ThrowableError(
LcpException.Parsing.Url(rel = LicenseDocument.Rel.publication.rawValue)
)
)
)

val resource = resourceFactory.create(url)
.getOrElse { return Try.failure(it.wrap()) }

val container = archiveFactory.create(resource, password = null)
.getOrElse { return Try.failure(it.wrap()) }

val publicationAsset = Asset.Container(
url.filename,
link.mediaType,
false,
container
)

return createResultAsset(publicationAsset, license)
}

private fun ResourceFactory.Error.wrap(): Publication.OpeningException =
when (this) {
is ResourceFactory.Error.NotAResource ->
Publication.OpeningException.NotFound()
is ResourceFactory.Error.Forbidden ->
Publication.OpeningException.Forbidden()
is ResourceFactory.Error.SchemeNotSupported ->
Publication.OpeningException.UnsupportedAsset()
}

private fun ArchiveFactory.Error.wrap(): Publication.OpeningException =
when (this) {
is ArchiveFactory.Error.FormatNotSupported ->
Publication.OpeningException.UnsupportedAsset()
is ArchiveFactory.Error.PasswordsNotSupported ->
Publication.OpeningException.UnsupportedAsset()
is ArchiveFactory.Error.ResourceReading ->
resourceException.wrap()
}

private fun Resource.Exception.wrap(): Publication.OpeningException =
when (this) {
is Resource.Exception.Forbidden ->
Publication.OpeningException.Forbidden(ThrowableError(this))
is Resource.Exception.NotFound ->
Publication.OpeningException.NotFound(ThrowableError(this))
Resource.Exception.Offline, is Resource.Exception.Unavailable ->
Publication.OpeningException.Unavailable(ThrowableError(this))
is Resource.Exception.Other, is Resource.Exception.BadRequest ->
Publication.OpeningException.Unexpected(this)
is Resource.Exception.OutOfMemory ->
Publication.OpeningException.OutOfMemory(ThrowableError(this))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@

package org.readium.r2.lcp

import org.readium.r2.shared.publication.ContentProtection
import org.readium.r2.shared.publication.Publication
import org.readium.r2.shared.publication.protection.ContentProtection
import org.readium.r2.shared.publication.services.ContentProtectionService

class LcpContentProtectionService(val license: LcpLicense?, override val error: LcpException?) : ContentProtectionService {
Expand Down
15 changes: 9 additions & 6 deletions readium/lcp/src/main/java/org/readium/r2/lcp/LcpDecryptor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,24 @@
package org.readium.r2.lcp

import java.io.IOException
import org.readium.r2.shared.error.Try
import org.readium.r2.shared.error.getOrElse
import org.readium.r2.shared.error.getOrThrow
import org.readium.r2.shared.extensions.coerceFirstNonNegative
import org.readium.r2.shared.extensions.inflate
import org.readium.r2.shared.extensions.requireLengthFitInt
import org.readium.r2.shared.fetcher.*
import org.readium.r2.shared.publication.Link
import org.readium.r2.shared.publication.encryption.encryption
import org.readium.r2.shared.util.Try
import org.readium.r2.shared.util.getOrElse
import org.readium.r2.shared.resource.Resource
import org.readium.r2.shared.resource.ResourceTry

/**
* Decrypts a resource protected with LCP.
*/
internal class LcpDecryptor(val license: LcpLicense?) {

fun transform(resource: Resource): Resource = LazyResource {
fun transform(resource: Fetcher.Resource): Fetcher.Resource = LazyResource {
// Checks if the resource is encrypted and whether the encryption schemes of the resource
// and the DRM license are the same.
val link = resource.link()
Expand All @@ -46,7 +49,7 @@ internal class LcpDecryptor(val license: LcpLicense?) {
* resource, for example when the resource is deflated before encryption.
*/
private class FullLcpResource(
resource: Resource,
resource: Fetcher.Resource,
private val license: LcpLicense
) : TransformingResource(resource) {

Expand All @@ -65,9 +68,9 @@ internal class LcpDecryptor(val license: LcpLicense?) {
* Supports random access for byte range requests, but the resource MUST NOT be deflated.
*/
private class CbcLcpResource(
private val resource: Resource,
private val resource: Fetcher.Resource,
private val license: LcpLicense
) : Resource {
) : Fetcher.Resource {

private class Cache(
var startIndex: Int? = null,
Expand Down
Loading