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 5 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 @@ -6,36 +6,102 @@

package org.readium.r2.lcp

import android.content.ContentResolver
import java.io.File
import org.readium.r2.lcp.auth.LcpDumbAuthentication
import org.readium.r2.lcp.auth.LcpPassphraseAuthentication
import org.readium.r2.lcp.license.model.LicenseDocument
import org.readium.r2.lcp.service.LcpLicensedAsset
import org.readium.r2.shared.fetcher.Fetcher
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.AssetFactory
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.asset.SimpleAsset
import org.readium.r2.shared.publication.services.contentProtectionServiceFactory
import org.readium.r2.shared.util.Try
import org.readium.r2.shared.util.Url
import org.readium.r2.shared.util.mediatype.AssetType
import org.readium.r2.shared.util.mediatype.MediaType

internal class LcpContentProtection(
private val lcpService: LcpService,
private val authentication: LcpAuthenticating
private val authentication: LcpAuthenticating,
private val assetFactory: AssetFactory
) : ContentProtection {

override suspend fun open(
url: Url,
mediaType: MediaType,
assetType: AssetType,
credentials: String?,
allowUserInteraction: Boolean,
sender: Any?
): Try<ContentProtection.ProtectedAsset, Publication.OpeningException>? {
if (url.scheme != ContentResolver.SCHEME_FILE || mediaType != MediaType.LCP_LICENSE_DOCUMENT) {
return null
}

val licenseFile = File(url.path)

val asset = try {
remoteAssetForLicenseThrowing(licenseFile)
} catch (e: Exception) {
// FIXME: random choice of exception
val exception = Publication.OpeningException.ParsingFailed(LcpException.wrap(e))
return Try.failure(exception)
}

return open(asset, credentials, allowUserInteraction, sender)
}

override suspend fun open(
asset: PublicationAsset,
credentials: String?,
allowUserInteraction: Boolean,
sender: Any?
): Try<ContentProtection.ProtectedAsset, Publication.OpeningException>? {
val license = retrieveLicense(asset, credentials, allowUserInteraction, sender)
val license = retrieveLicense(asset, asset.fetcher, credentials, allowUserInteraction, sender)
?: return null
return createProtectedAsset(asset, license)
return createProtectedAsset(asset, asset.fetcher, license)
}

private suspend fun remoteAssetForLicenseThrowing(licenseFile: File): PublicationAsset {
// Update the license file to get a fresh publication URL.
val license = lcpService.retrieveLicense(licenseFile, LcpDumbAuthentication(), false)
.getOrNull()

val licenseDoc = license?.license
?: LicenseDocument(licenseFile.readBytes())

val link = checkNotNull(licenseDoc.link(LicenseDocument.Rel.publication))
val url = try {
Url(link.url.toString()) ?: throw IllegalStateException()
} catch (e: Exception) {
throw LcpException.Parsing.Url(rel = LicenseDocument.Rel.publication.rawValue)
}

val asset = assetFactory.createAsset(
url,
link.mediaType,
AssetType.Archive
).getOrThrow()

return LcpLicensedAsset(
url,
link.mediaType,
licenseFile,
license,
asset.fetcher
)
}

/* Returns null if the publication is not protected by LCP. */
private suspend fun retrieveLicense(
asset: PublicationAsset,
fetcher: Fetcher,
credentials: String?,
allowUserInteraction: Boolean,
sender: Any?
Expand All @@ -48,45 +114,33 @@ internal class LcpContentProtection(
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
lcpService.retrieveLicense(fetcher, asset.mediaType, authentication, allowUserInteraction, sender)
}

return license?.takeUnless { result ->
return license.takeUnless { result ->
result is Try.Failure<*, *> && result.exception is LcpException.Container
}
}

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

val newFetcher = TransformingFetcher(
originalAsset.fetcher,
fetcher,
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 newAsset = SimpleAsset(asset.name, asset.mediaType, newFetcher)

val protectedFile = ContentProtection.ProtectedAsset(
asset = newAsset,
Expand Down
26 changes: 10 additions & 16 deletions readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,11 @@ import org.readium.r2.lcp.service.PassphrasesRepository
import org.readium.r2.lcp.service.PassphrasesService
import org.readium.r2.shared.fetcher.Fetcher
import org.readium.r2.shared.publication.ContentProtection
import org.readium.r2.shared.publication.asset.PublicationAsset
import org.readium.r2.shared.publication.asset.AssetFactory
import org.readium.r2.shared.publication.asset.DefaultAssetFactory
import org.readium.r2.shared.util.Try
import org.readium.r2.shared.util.archive.ArchiveFactory
import org.readium.r2.shared.util.archive.DefaultArchiveFactory
import org.readium.r2.shared.util.http.DefaultHttpClient
import org.readium.r2.shared.util.http.HttpClient
import org.readium.r2.shared.util.mediatype.MediaType
import org.readium.r2.shared.util.mediatype.MediaTypeRetriever

/**
* Service used to acquire and open publications protected with LCP.
Expand Down Expand Up @@ -114,13 +112,9 @@ interface LcpService {
* LCP license. The default implementation [LcpDialogAuthentication] presents a dialog to the
* user to enter their passphrase.
*/
fun contentProtection(authentication: LcpAuthenticating = LcpDialogAuthentication()): ContentProtection =
LcpContentProtection(this, authentication)

/**
* Builds a [PublicationAsset] to open a LCP-protected publication from its license file.
*/
suspend fun remoteAssetForLicense(license: File): Try<PublicationAsset, LcpException>
fun contentProtection(
authentication: LcpAuthenticating = LcpDialogAuthentication(),
): ContentProtection

/**
* Information about an acquired publication protected with LCP.
Expand All @@ -145,8 +139,8 @@ interface LcpService {
*/
operator fun invoke(
context: Context,
archiveFactory: ArchiveFactory = DefaultArchiveFactory(),
httpClient: HttpClient = DefaultHttpClient()
assetFactory: AssetFactory = DefaultAssetFactory(),
mediaTypeRetriever: MediaTypeRetriever = MediaTypeRetriever()
): LcpService? {
if (!LcpClient.isAvailable())
return null
Expand All @@ -166,8 +160,8 @@ interface LcpService {
network = network,
passphrases = passphrases,
context = context,
archiveFactory = archiveFactory,
httpClient = httpClient
assetFactory = assetFactory,
mediaTypeRetriever = mediaTypeRetriever
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ package org.readium.r2.lcp.license.container
import kotlinx.coroutines.runBlocking
import org.readium.r2.lcp.LcpException
import org.readium.r2.lcp.license.model.LicenseDocument
import org.readium.r2.shared.util.archive.Archive
import org.readium.r2.shared.util.archive.Package

/**
* Access to a License Document stored in a read-only ZIP archive.
*/
internal class ArchiveLicenseContainer(
private val archive: Archive,
private val archive: Package,
private val entryPath: String,
) : LicenseContainer {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import org.readium.r2.lcp.license.model.LicenseDocument
import org.readium.r2.shared.extensions.addPrefix
import org.readium.r2.shared.fetcher.Fetcher
import org.readium.r2.shared.util.mediatype.MediaType
import org.readium.r2.shared.util.mediatype.MediaTypeRetriever

private const val LICENSE_IN_EPUB = "META-INF/license.lcpl"

Expand All @@ -30,9 +31,10 @@ internal interface LicenseContainer {

internal suspend fun createLicenseContainer(
filepath: String,
mediaTypes: List<String> = emptyList()
mediaTypes: List<String> = emptyList(),
mediaTypeRetriever: MediaTypeRetriever
): LicenseContainer {
val mediaType = MediaType.ofFile(filepath, mediaTypes = mediaTypes, fileExtensions = emptyList())
val mediaType = mediaTypeRetriever.ofFile(filepath, mediaTypes = mediaTypes, fileExtensions = emptyList())
?: throw LcpException.Container.OpenFailed
return createLicenseContainer(filepath, mediaType)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,17 @@ import java.io.File
import org.readium.r2.lcp.LcpLicense
import org.readium.r2.shared.fetcher.Fetcher
import org.readium.r2.shared.publication.asset.PublicationAsset
import org.readium.r2.shared.util.Url
import org.readium.r2.shared.util.mediatype.MediaType

data class LcpLicensedAsset(
override val name: String,
val url: Url,
override val mediaType: MediaType,
override val fetcher: Fetcher,
val licenseFile: File,
val license: LcpLicense?
) : PublicationAsset
val license: LcpLicense?,
override val fetcher: Fetcher
) : PublicationAsset {

override val name: String =
url.file
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,19 @@ import android.content.Context
import java.io.File
import kotlin.coroutines.resume
import kotlinx.coroutines.*
import org.readium.r2.lcp.LcpAuthenticating
import org.readium.r2.lcp.LcpException
import org.readium.r2.lcp.LcpLicense
import org.readium.r2.lcp.LcpService
import org.readium.r2.lcp.auth.LcpDumbAuthentication
import org.readium.r2.lcp.*
import org.readium.r2.lcp.license.License
import org.readium.r2.lcp.license.LicenseValidation
import org.readium.r2.lcp.license.container.LicenseContainer
import org.readium.r2.lcp.license.container.createLicenseContainer
import org.readium.r2.lcp.license.model.LicenseDocument
import org.readium.r2.shared.extensions.tryOr
import org.readium.r2.shared.fetcher.Fetcher
import org.readium.r2.shared.publication.asset.PublicationAsset
import org.readium.r2.shared.publication.asset.RemoteAsset
import org.readium.r2.shared.publication.ContentProtection
import org.readium.r2.shared.publication.asset.AssetFactory
import org.readium.r2.shared.util.Try
import org.readium.r2.shared.util.Url
import org.readium.r2.shared.util.archive.ArchiveFactory
import org.readium.r2.shared.util.http.HttpClient
import org.readium.r2.shared.util.mediatype.MediaType
import org.readium.r2.shared.util.mediatype.MediaTypeRetriever
import timber.log.Timber

internal class LicensesService(
Expand All @@ -41,50 +35,20 @@ internal class LicensesService(
private val network: NetworkService,
private val passphrases: PassphrasesService,
private val context: Context,
private val archiveFactory: ArchiveFactory,
private val httpClient: HttpClient
private val assetFactory: AssetFactory,
private val mediaTypeRetriever: MediaTypeRetriever
) : LcpService, CoroutineScope by MainScope() {

override suspend fun isLcpProtected(file: File): Boolean =
tryOr(false) {
createLicenseContainer(file.path).read()
createLicenseContainer(file.path, mediaTypeRetriever = mediaTypeRetriever).read()
true
}

override suspend fun remoteAssetForLicense(license: File): Try<PublicationAsset, LcpException> {
return try {
Try.success(remoteAssetForLicenseThrowing(license))
} catch (e: Exception) {
Try.failure(LcpException.wrap(e))
}
}

private suspend fun remoteAssetForLicenseThrowing(licenseFile: File): PublicationAsset {
// Update the license file to get a fresh publication URL.
val license = retrieveLicense(licenseFile, LcpDumbAuthentication(), false)
.getOrNull()

val licenseDoc = license?.license
?: LicenseDocument(licenseFile.readBytes())

val link = checkNotNull(licenseDoc.link(LicenseDocument.Rel.publication))
val url = try {
Url(link.url.toString()) ?: throw IllegalStateException()
} catch (e: Exception) {
throw LcpException.Parsing.Url(rel = LicenseDocument.Rel.publication.rawValue)
}
val baseAsset = RemoteAsset.Factory(archiveFactory, httpClient)
.createAsset(url, link.mediaType)
.getOrThrow()

return LcpLicensedAsset(
baseAsset.name,
baseAsset.mediaType,
baseAsset.fetcher,
licenseFile,
license
)
}
override fun contentProtection(
authentication: LcpAuthenticating,
): ContentProtection =
LcpContentProtection(this, authentication, assetFactory)

override suspend fun acquirePublication(lcpl: ByteArray, onProgress: (Double) -> Unit): Try<LcpService.AcquiredPublication, LcpException> =
try {
Expand All @@ -102,7 +66,7 @@ internal class LicensesService(
sender: Any?
): Try<LcpLicense, LcpException> =
try {
val container = createLicenseContainer(file.path)
val container = createLicenseContainer(file.path, mediaTypeRetriever = mediaTypeRetriever)
val license = retrieveLicense(container, authentication, allowUserInteraction, true, sender)
Try.success(license)
} catch (e: Exception) {
Expand Down Expand Up @@ -238,7 +202,9 @@ internal class LicensesService(
}
Timber.i("LCP destination $destination")

val mediaType = network.download(url, destination, mediaType = link.type, onProgress = onProgress) ?: MediaType.of(mediaType = link.type) ?: MediaType.EPUB
val mediaType = network.download(url, destination, mediaType = link.type, onProgress = onProgress)
?: mediaTypeRetriever.of(mediaType = link.type)
?: MediaType.EPUB

// Saves the License Document into the downloaded publication
val container = createLicenseContainer(destination.path, mediaType)
Expand Down
Loading