diff --git a/r2-streamer/src/main/java/org/readium/r2/streamer/fetcher/ContentFilter.kt b/r2-streamer/src/main/java/org/readium/r2/streamer/fetcher/ContentFilter.kt index ade77b5c..3412dd93 100644 --- a/r2-streamer/src/main/java/org/readium/r2/streamer/fetcher/ContentFilter.kt +++ b/r2-streamer/src/main/java/org/readium/r2/streamer/fetcher/ContentFilter.kt @@ -13,6 +13,7 @@ import org.json.JSONArray import org.json.JSONObject import org.readium.r2.shared.Injectable import org.readium.r2.shared.ReadiumCSSName +import org.readium.r2.shared.publication.ContentLayout import org.readium.r2.shared.publication.Publication import org.readium.r2.shared.publication.epub.EpubLayout import org.readium.r2.shared.publication.epub.layout @@ -85,17 +86,18 @@ internal class ContentFiltersEpub(private val userPropertiesPath: String?, priva if (endHeadIndex == -1) return stream - val cssStyle = publication.cssStyle + val contentLayout = publication.contentLayout val endIncludes = mutableListOf() val beginIncludes = mutableListOf() beginIncludes.add("") - beginIncludes.add(getHtmlLink("/"+ Injectable.Style.rawValue +"/$cssStyle-before.css")) - beginIncludes.add(getHtmlLink("/"+ Injectable.Style.rawValue +"/$cssStyle-default.css")) - endIncludes.add(getHtmlLink("/"+ Injectable.Style.rawValue +"/$cssStyle-after.css")) - endIncludes.add(getHtmlScript("/"+ Injectable.Script.rawValue +"/touchHandling.js")) - endIncludes.add(getHtmlScript("/"+ Injectable.Script.rawValue +"/utils.js")) + beginIncludes.add(getHtmlLink("/assets/readium-css/${contentLayout.readiumCSSPath}ReadiumCSS-before.css")) + endIncludes.add(getHtmlLink("/assets/readium-css/${contentLayout.readiumCSSPath}ReadiumCSS-after.css")) + endIncludes.add(getHtmlScript("/assets/scripts/touchHandling.js")) + endIncludes.add(getHtmlScript("/assets/scripts/utils.js")) + endIncludes.add(getHtmlScript("/assets/scripts/crypto-sha256.js")) + endIncludes.add(getHtmlScript("/assets/scripts/highlight.js")) customResources?.let { // Inject all custom resourses @@ -120,7 +122,7 @@ internal class ContentFiltersEpub(private val userPropertiesPath: String?, priva resourceHtml = StringBuilder(resourceHtml).insert(endHeadIndex, element).toString() endHeadIndex += element.length } - resourceHtml = StringBuilder(resourceHtml).insert(endHeadIndex, getHtmlFont("/fonts/OpenDyslexic-Regular.otf")).toString() + resourceHtml = StringBuilder(resourceHtml).insert(endHeadIndex, getHtmlFont(fontFamily = "OpenDyslexic", href = "/assets/fonts/OpenDyslexic-Regular.otf")).toString() resourceHtml = StringBuilder(resourceHtml).insert(endHeadIndex, "\n").toString() // Inject userProperties @@ -186,10 +188,10 @@ internal class ContentFiltersEpub(private val userPropertiesPath: String?, priva return resourceHtml.toByteArray().inputStream() } - private fun getHtmlFont(resourceName: String): String { - val prefix = "\n" - return prefix + resourceName + suffix + return prefix + href + suffix } private fun getHtmlLink(resourceName: String): String { @@ -319,10 +321,15 @@ internal class ContentFiltersEpub(private val userPropertiesPath: String?, priva return string } + private val ContentLayout.readiumCSSPath: String get() = when(this) { + ContentLayout.LTR -> "" + ContentLayout.RTL -> "rtl/" + ContentLayout.CJK_VERTICAL -> "cjk-vertical/" + ContentLayout.CJK_HORIZONTAL -> "cjk-horizontal/" + } } - internal class ContentFiltersCbz : ContentFilters /** Content filter for LCP protected packages (except EPUB). */ @@ -343,4 +350,4 @@ internal class ContentFiltersLcp : ContentFilters { return DrmDecoder().decoding(inputStream, resourceLink, container.drm).readBytes() } -} \ No newline at end of file +} diff --git a/r2-streamer/src/main/java/org/readium/r2/streamer/server/Assets.kt b/r2-streamer/src/main/java/org/readium/r2/streamer/server/Assets.kt new file mode 100644 index 00000000..f7aea350 --- /dev/null +++ b/r2-streamer/src/main/java/org/readium/r2/streamer/server/Assets.kt @@ -0,0 +1,62 @@ +/* + * Module: r2-streamer-kotlin + * Developers: Mickaël Menu + * + * Copyright (c) 2020. Readium Foundation. All rights reserved. + * Use of this source code is governed by a BSD-style license which is detailed in the + * LICENSE file present in the project repository where this source code is maintained. + */ + +package org.readium.r2.streamer.server + +import android.content.res.AssetManager +import android.net.Uri +import org.readium.r2.shared.extensions.isParentOf +import org.readium.r2.shared.format.Format +import org.readium.r2.shared.format.MediaType +import java.io.File +import java.io.InputStream + + +/** + * Files to be served from the application's assets. + * + * @param basePath Base path (ignoring host) from where the files are served. + * @param fallbackMediaType Media type which will be used for responses when it can't be determined + * from the served file. + */ +internal class Assets( + private val assetManager: AssetManager, + private val basePath: String, + private val fallbackMediaType: MediaType = MediaType.BINARY +) { + private val assets: MutableList> = mutableListOf() + + fun add(href: String, path: String) { + // Inserts at the beginning to take precedence over already registered assets. + assets.add(0, Pair(href, File("/$path").canonicalFile)) + } + + fun find(uri: Uri): ServedAsset? { + val path = uri.path?.removePrefix(basePath) ?: return null + + for ((href, file) in assets) { + if (path.startsWith(href)) { + val requestedFile = File(file, path.removePrefix(href)).canonicalFile + // Makes sure that the requested file is `file` or one of its descendant. + if (file == requestedFile || file.isParentOf(requestedFile)) { + val mediaType = Format.of(fileExtension = requestedFile.extension)?.mediaType ?: fallbackMediaType + return ServedAsset(assetManager.open(requestedFile.path.removePrefix("/")), mediaType) + } + } + } + + return null + } + + data class ServedAsset( + val stream: InputStream, + val mediaType: MediaType + ) + +} diff --git a/r2-streamer/src/main/java/org/readium/r2/streamer/server/Files.kt b/r2-streamer/src/main/java/org/readium/r2/streamer/server/Files.kt new file mode 100644 index 00000000..7e404cb3 --- /dev/null +++ b/r2-streamer/src/main/java/org/readium/r2/streamer/server/Files.kt @@ -0,0 +1,60 @@ +/* + * Module: r2-streamer-kotlin + * Developers: Aferdita Muriqi, Clément Baumann + * + * Copyright (c) 2018. Readium Foundation. All rights reserved. + * Use of this source code is governed by a BSD-style license which is detailed in the + * LICENSE file present in the project repository where this source code is maintained. + */ + +package org.readium.r2.streamer.server + +import android.net.Uri +import org.readium.r2.shared.extensions.isParentOf +import org.readium.r2.shared.format.Format +import org.readium.r2.shared.format.MediaType +import java.io.File + +/** + * Files to be served from the file system. + * + * @param basePath Base path (ignoring host) from where the files are served. + * @param fallbackMediaType Media type which will be used for responses when it can't be determined + * from the served file. + */ +internal class Files( + private val basePath: String, + private val fallbackMediaType: MediaType = MediaType.BINARY +) { + private val files: MutableMap = mutableMapOf() + + operator fun set(href: String, file: File) { + files[href] = file.canonicalFile + } + + operator fun get(key: String): File? = files[key] + + fun find(uri: Uri): ServedFile? { + val path = uri.path?.removePrefix(basePath) ?: return null + + for ((href, file) in files) { + if (path.startsWith(href)) { + val requestedFile = File(file, path.removePrefix(href)).canonicalFile + // Makes sure that the requested file is `file` or one of its descendant. + if (file.isParentOf(requestedFile)) { + return ServedFile(requestedFile, fallbackMediaType) + } + } + } + + return null + } + + data class ServedFile( + val file: File, + private val fallbackMediaType: MediaType + ) { + val mediaType: MediaType get() = Format.of(file)?.mediaType ?: fallbackMediaType + } + +} diff --git a/r2-streamer/src/main/java/org/readium/r2/streamer/server/Fonts.kt b/r2-streamer/src/main/java/org/readium/r2/streamer/server/Fonts.kt deleted file mode 100644 index b93f2f04..00000000 --- a/r2-streamer/src/main/java/org/readium/r2/streamer/server/Fonts.kt +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Module: r2-streamer-kotlin - * Developers: Aferdita Muriqi, Clément Baumann - * - * Copyright (c) 2018. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.streamer.server - -import java.io.File - -class Fonts { - private val fonts: MutableMap = mutableMapOf() - - fun add(key: String, body: File) { - fonts[key] = body - } - - fun get(key: String): File { - return fonts[key]!! - } -} diff --git a/r2-streamer/src/main/java/org/readium/r2/streamer/server/Resources.kt b/r2-streamer/src/main/java/org/readium/r2/streamer/server/Resources.kt index 4f4eacbe..70bc8787 100644 --- a/r2-streamer/src/main/java/org/readium/r2/streamer/server/Resources.kt +++ b/r2-streamer/src/main/java/org/readium/r2/streamer/server/Resources.kt @@ -22,13 +22,10 @@ class Resources { } } - fun getPair(key: String): Any { - return resources[key] ?: "" - } - - fun get(key: String): String = + fun get(key: String): String? = when (val resource = resources[key]) { is Pair<*, *> -> resource.first as? String else -> resource as? String - } ?: "" + } + } \ No newline at end of file diff --git a/r2-streamer/src/main/java/org/readium/r2/streamer/server/Server.kt b/r2-streamer/src/main/java/org/readium/r2/streamer/server/Server.kt index 98ffe1bc..b3a134c1 100755 --- a/r2-streamer/src/main/java/org/readium/r2/streamer/server/Server.kt +++ b/r2-streamer/src/main/java/org/readium/r2/streamer/server/Server.kt @@ -26,11 +26,10 @@ import java.net.URL import java.net.URLDecoder import java.util.* -class Server(port: Int) : AbstractServer(port) +class Server(port: Int, context: Context) : AbstractServer(port, context.applicationContext) -abstract class AbstractServer(private var port: Int) : RouterNanoHTTPD("127.0.0.1", port) { +abstract class AbstractServer(private var port: Int, private val context: Context) : RouterNanoHTTPD("127.0.0.1", port) { - // private val SEARCH_QUERY_HANDLE = "/search" private val MANIFEST_HANDLE = "/manifest" private val JSON_MANIFEST_HANDLE = "/manifest.json" private val MANIFEST_ITEM_HANDLE = "/(.*)" @@ -38,12 +37,19 @@ abstract class AbstractServer(private var port: Int) : RouterNanoHTTPD("127.0.0. private val CSS_HANDLE = "/"+ Injectable.Style.rawValue +"/(.*)" private val JS_HANDLE = "/"+ Injectable.Script.rawValue +"/(.*)" private val FONT_HANDLE = "/"+ Injectable.Font.rawValue +"/(.*)" + private val ASSETS_HANDLE = "/assets/(.*)" private var containsMediaOverlay = false private val resources = Resources() private val customResources = Resources() + private val assets = Assets(context.assets, basePath = "/assets/") + private val fonts = Files(basePath = "/${Injectable.Style}/") - private val fonts = Fonts() + init { + assets.add(href = "readium-css", path = "readium/readium-css") + assets.add(href = "scripts", path = "readium/scripts") + assets.add(href = "fonts", path = "readium/fonts") + } private fun addResource(name: String, body: String, custom: Boolean = false, injectable: Injectable? = null) { if (custom) { @@ -57,103 +63,7 @@ abstract class AbstractServer(private var port: Int) : RouterNanoHTTPD("127.0.0. dir.mkdirs() inputStream.toFile(context.filesDir.path + "/" + Injectable.Font.rawValue + "/" + name) val file = File(context.filesDir.path + "/" + Injectable.Font.rawValue + "/" + name) - fonts.add(name, file) - } - - fun loadReadiumCSSResources(assets: AssetManager) { - try { - addResource("ltr-after.css", Scanner(assets.open("static/"+ Injectable.Style.rawValue +"/ltr/ReadiumCSS-after.css"), "utf-8") - .useDelimiter("\\A").next()) - } catch (e: IOException) { - if (DEBUG) Timber.d(e) - } - try { - addResource("ltr-before.css", Scanner(assets.open("static/"+ Injectable.Style.rawValue +"/ltr/ReadiumCSS-before.css"), "utf-8") - .useDelimiter("\\A").next()) - } catch (e: IOException) { - if (DEBUG) Timber.d(e) - } - try { - addResource("ltr-default.css", Scanner(assets.open("static/"+ Injectable.Style.rawValue +"/ltr/ReadiumCSS-default.css"), "utf-8") - .useDelimiter("\\A").next()) - } catch (e: IOException) { - if (DEBUG) Timber.d(e) - } - try { - addResource("rtl-after.css", Scanner(assets.open("static/"+ Injectable.Style.rawValue +"/rtl/ReadiumCSS-after.css"), "utf-8") - .useDelimiter("\\A").next()) - } catch (e: IOException) { - if (DEBUG) Timber.d(e) - } - try { - addResource("rtl-before.css", Scanner(assets.open("static/"+ Injectable.Style.rawValue +"/rtl/ReadiumCSS-before.css"), "utf-8") - .useDelimiter("\\A").next()) - } catch (e: IOException) { - if (DEBUG) Timber.d(e) - } - try { - addResource("rtl-default.css", Scanner(assets.open("static/"+ Injectable.Style.rawValue +"/rtl/ReadiumCSS-default.css"), "utf-8") - .useDelimiter("\\A").next()) - } catch (e: IOException) { - if (DEBUG) Timber.d(e) - } - try { - addResource("cjk-vertical-after.css", Scanner(assets.open("static/"+ Injectable.Style.rawValue +"/cjk-vertical/ReadiumCSS-after.css"), "utf-8") - .useDelimiter("\\A").next()) - } catch (e: IOException) { - if (DEBUG) Timber.d(e) - } - try { - addResource("cjk-vertical-before.css", Scanner(assets.open("static/"+ Injectable.Style.rawValue +"/cjk-vertical/ReadiumCSS-before.css"), "utf-8") - .useDelimiter("\\A").next()) - } catch (e: IOException) { - if (DEBUG) Timber.d(e) - } - try { - addResource("cjk-vertical-default.css", Scanner(assets.open("static/"+ Injectable.Style.rawValue +"/cjk-vertical/ReadiumCSS-default.css"), "utf-8") - .useDelimiter("\\A").next()) - } catch (e: IOException) { - if (DEBUG) Timber.d(e) - } - try { - addResource("cjk-horizontal-after.css", Scanner(assets.open("static/"+ Injectable.Style.rawValue +"/cjk-horizontal/ReadiumCSS-after.css"), "utf-8") - .useDelimiter("\\A").next()) - } catch (e: IOException) { - if (DEBUG) Timber.d(e) - } - try { - addResource("cjk-horizontal-before.css", Scanner(assets.open("static/"+ Injectable.Style.rawValue +"/cjk-horizontal/ReadiumCSS-before.css"), "utf-8") - .useDelimiter("\\A").next()) - } catch (e: IOException) { - if (DEBUG) Timber.d(e) - } - try { - addResource("cjk-horizontal-default.css", Scanner(assets.open("static/"+ Injectable.Style.rawValue +"/cjk-horizontal/ReadiumCSS-default.css"), "utf-8") - .useDelimiter("\\A").next()) - } catch (e: IOException) { - if (DEBUG) Timber.d(e) - } - } - fun loadR2ScriptResources(assets: AssetManager) { - try { - addResource("touchHandling.js", Scanner(assets.open(Injectable.Script.rawValue + "/touchHandling.js"), "utf-8") - .useDelimiter("\\A").next()) - } catch (e: IOException) { - if (DEBUG) Timber.d(e) - } - try { - addResource("utils.js", Scanner(assets.open(Injectable.Script.rawValue + "/utils.js"), "utf-8") - .useDelimiter("\\A").next()) - } catch (e: IOException) { - if (DEBUG) Timber.d(e) - } - } - fun loadR2FontResources(assets: AssetManager, context: Context) { - try { - addFont("OpenDyslexic-Regular.otf", assets.open("static/"+ Injectable.Font.rawValue +"/OpenDyslexic-Regular.otf"), context) - } catch (e: IOException) { - if (DEBUG) Timber.d(e) - } + fonts[name] = file } fun loadCustomResource(inputStream: InputStream, fileName: String, injectable: Injectable) { @@ -191,22 +101,23 @@ abstract class AbstractServer(private var port: Int) : RouterNanoHTTPD("127.0.0. } addRoute(basePath + JSON_MANIFEST_HANDLE, ManifestHandler::class.java, fetcher) addRoute(basePath + MANIFEST_HANDLE, ManifestHandler::class.java, fetcher) - addRoute(basePath + MANIFEST_ITEM_HANDLE, ResourceHandler::class.java, fetcher) - addRoute(JS_HANDLE, JSHandler::class.java, resources) - addRoute(CSS_HANDLE, CSSHandler::class.java, resources) - addRoute(FONT_HANDLE, FontHandler::class.java, fonts) + addRoute(basePath + MANIFEST_ITEM_HANDLE, PublicationResourceHandler::class.java, fetcher) + addRoute(ASSETS_HANDLE, AssetHandler::class.java, assets) + addRoute(JS_HANDLE, ResourceHandler::class.java, resources) + addRoute(CSS_HANDLE, ResourceHandler::class.java, resources) + addRoute(FONT_HANDLE, FileHandler::class.java, fonts) } - /* FIXME: To review once the media-overlays will be supported in the Publication model - private fun addLinks(publication: Publication, filePath: String) { - containsMediaOverlay = false - for (link in publication.otherLinks) { - if (link.rel.contains("media-overlay")) { - containsMediaOverlay = true - link.href = link.href?.replace("port", "127.0.0.1:$listeningPort$filePath") - } - } - } */ + // FIXME: To review once the media-overlays will be supported in the Publication model +// private fun addLinks(publication: Publication, filePath: String) { +// containsMediaOverlay = false +// for (link in publication.otherLinks) { +// if (link.rel.contains("media-overlay")) { +// containsMediaOverlay = true +// link.href = link.href?.replace("port", "127.0.0.1:$listeningPort$filePath") +// } +// } +// } private fun InputStream.toFile(path: String) { use { input -> @@ -214,5 +125,12 @@ abstract class AbstractServer(private var port: Int) : RouterNanoHTTPD("127.0.0. } } + @Deprecated("This is not needed anymore") + fun loadReadiumCSSResources(assets: AssetManager) {} + @Deprecated("This is not needed anymore") + fun loadR2ScriptResources(assets: AssetManager) {} + @Deprecated("This is not needed anymore") + fun loadR2FontResources(assets: AssetManager, context: Context) {} + } diff --git a/r2-streamer/src/main/java/org/readium/r2/streamer/server/handler/AssetHandler.kt b/r2-streamer/src/main/java/org/readium/r2/streamer/server/handler/AssetHandler.kt new file mode 100644 index 00000000..ddb4cd5f --- /dev/null +++ b/r2-streamer/src/main/java/org/readium/r2/streamer/server/handler/AssetHandler.kt @@ -0,0 +1,30 @@ +/* + * Module: r2-streamer-kotlin + * Developers: Mickaël Menu + * + * Copyright (c) 2020. Readium Foundation. All rights reserved. + * Use of this source code is governed by a BSD-style license which is detailed in the + * LICENSE file present in the project repository where this source code is maintained. + */ + +package org.readium.r2.streamer.server.handler + +import android.net.Uri +import org.nanohttpd.protocols.http.response.Response +import org.nanohttpd.router.RouterNanoHTTPD +import org.readium.r2.streamer.server.Assets + +/** + * Serves files from the local file system. + * + * The NanoHTTPD init parameter must be an instance of `Assets`. + */ +internal class AssetHandler : BaseHandler() { + + override fun handle(resource: RouterNanoHTTPD.UriResource, uri: Uri, parameters: Map?): Response { + val assets = resource.initParameter(Assets::class.java) + val asset = assets.find(uri) ?: return notFoundResponse + return createResponse(mediaType = asset.mediaType, body = asset.stream) + } + +} diff --git a/r2-streamer/src/main/java/org/readium/r2/streamer/server/handler/BaseHandler.kt b/r2-streamer/src/main/java/org/readium/r2/streamer/server/handler/BaseHandler.kt new file mode 100644 index 00000000..1ca0d4ba --- /dev/null +++ b/r2-streamer/src/main/java/org/readium/r2/streamer/server/handler/BaseHandler.kt @@ -0,0 +1,70 @@ +/* + * Module: r2-streamer-kotlin + * Developers: Mickaël Menu + * + * Copyright (c) 2020. Readium Foundation. All rights reserved. + * Use of this source code is governed by a BSD-style license which is detailed in the + * LICENSE file present in the project repository where this source code is maintained. + */ + +package org.readium.r2.streamer.server.handler + +import android.net.Uri +import org.nanohttpd.protocols.http.IHTTPSession +import org.nanohttpd.protocols.http.response.IStatus +import org.nanohttpd.protocols.http.response.Response +import org.nanohttpd.protocols.http.response.Status +import org.nanohttpd.router.RouterNanoHTTPD +import org.readium.r2.shared.format.MediaType +import org.readium.r2.streamer.BuildConfig +import timber.log.Timber +import java.io.FileNotFoundException +import java.io.InputStream + +internal abstract class BaseHandler : RouterNanoHTTPD.DefaultHandler() { + + override fun getMimeType(): String? = null + override fun getText(): String = "" + override fun getStatus(): IStatus = Status.OK + + override fun get(uriResource: RouterNanoHTTPD.UriResource?, urlParams: Map?, session: IHTTPSession?): Response { + uriResource ?: return notFoundResponse + session ?: return notFoundResponse + + if (BuildConfig.DEBUG) Timber.v("Method: ${session.method}, URL: ${session.uri}") + + return try { + val uri = Uri.parse(session.uri) + handle(resource = uriResource, uri = uri, parameters = urlParams) + + } catch (e: FileNotFoundException) { + if (BuildConfig.DEBUG) Timber.e( "Server handler error: %s", e.toString()) + notFoundResponse + + } catch (e: Exception) { + if (BuildConfig.DEBUG) Timber.e( "Server handler error: %s", e.toString()) + createErrorResponse(Status.INTERNAL_ERROR) + } + } + + abstract fun handle(resource: RouterNanoHTTPD.UriResource, uri: Uri, parameters: Map?): Response + + fun createResponse(mediaType: MediaType, body: String): Response = + createResponse(mediaType, body.toByteArray()) + + fun createResponse(mediaType: MediaType, body: ByteArray): Response = + Response.newFixedLengthResponse(Status.OK, mediaType.toString(), body).apply { + addHeader("Accept-Ranges", "bytes") + } + + fun createResponse(mediaType: MediaType, body: InputStream): Response = + Response.newChunkedResponse(Status.OK, mediaType.toString(), body).apply { + addHeader("Accept-Ranges", "bytes") + } + + fun createErrorResponse(status: Status) = + Response.newFixedLengthResponse(status, "text/html", "") + + val notFoundResponse: Response get() = createErrorResponse(Status.NOT_FOUND) + +} diff --git a/r2-streamer/src/main/java/org/readium/r2/streamer/server/handler/CSSHandler.kt b/r2-streamer/src/main/java/org/readium/r2/streamer/server/handler/CSSHandler.kt deleted file mode 100644 index 753232f7..00000000 --- a/r2-streamer/src/main/java/org/readium/r2/streamer/server/handler/CSSHandler.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Module: r2-streamer-kotlin - * Developers: Aferdita Muriqi, Clément Baumann - * - * Copyright (c) 2018. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.streamer.server.handler - -import org.nanohttpd.protocols.http.IHTTPSession -import org.nanohttpd.protocols.http.response.IStatus -import org.nanohttpd.protocols.http.response.Response -import org.nanohttpd.protocols.http.response.Response.newFixedLengthResponse -import org.nanohttpd.protocols.http.response.Status -import org.nanohttpd.router.RouterNanoHTTPD -import org.readium.r2.streamer.BuildConfig.DEBUG -import org.readium.r2.streamer.server.Resources -import timber.log.Timber - - -class CSSHandler : RouterNanoHTTPD.DefaultHandler() { - - override fun getMimeType(): String? { - return null - } - - override fun getText(): String { - return ResponseStatus.FAILURE_RESPONSE - } - - override fun getStatus(): IStatus { - return Status.OK - } - - override fun get(uriResource: RouterNanoHTTPD.UriResource?, urlParams: Map?, session: IHTTPSession?): Response { - - val method = session!!.method - var uri = session.uri - - if (DEBUG) Timber.v("Method: $method, Url: $uri") - - return try { - val lastSlashIndex = uri.lastIndexOf('/') - uri = uri.substring(lastSlashIndex + 1, uri.length) - val resources = uriResource!!.initParameter(Resources::class.java) - val x = createResponse(Status.OK, "text/css", resources.get(uri)) - x - } catch (e: Exception) { - if (DEBUG) Timber.e( " Exception %s", e.toString()) - newFixedLengthResponse(Status.INTERNAL_ERROR, mimeType, ResponseStatus.FAILURE_RESPONSE) - } - - } - - private fun createResponse(status: Status, mimeType: String, message: String): Response { - val response = newFixedLengthResponse(status, mimeType, message) - response.addHeader("Accept-Ranges", "bytes") - return response - } -} diff --git a/r2-streamer/src/main/java/org/readium/r2/streamer/server/handler/FileHandler.kt b/r2-streamer/src/main/java/org/readium/r2/streamer/server/handler/FileHandler.kt new file mode 100644 index 00000000..8d9e4ef7 --- /dev/null +++ b/r2-streamer/src/main/java/org/readium/r2/streamer/server/handler/FileHandler.kt @@ -0,0 +1,30 @@ +/* + * Module: r2-streamer-kotlin + * Developers: Mickaël Menu + * + * Copyright (c) 2020. Readium Foundation. All rights reserved. + * Use of this source code is governed by a BSD-style license which is detailed in the + * LICENSE file present in the project repository where this source code is maintained. + */ + +package org.readium.r2.streamer.server.handler + +import android.net.Uri +import org.nanohttpd.protocols.http.response.Response +import org.nanohttpd.router.RouterNanoHTTPD +import org.readium.r2.streamer.server.Files + +/** + * Serves files from the local file system. + * + * The NanoHTTPD init parameter must be an instance of `Files`. + */ +internal class FileHandler : BaseHandler() { + + override fun handle(resource: RouterNanoHTTPD.UriResource, uri: Uri, parameters: Map?): Response { + val files = resource.initParameter(Files::class.java) + val file = files.find(uri) ?: return notFoundResponse + return createResponse(mediaType = file.mediaType, body = file.file.inputStream()) + } + +} diff --git a/r2-streamer/src/main/java/org/readium/r2/streamer/server/handler/FontHandler.kt b/r2-streamer/src/main/java/org/readium/r2/streamer/server/handler/FontHandler.kt deleted file mode 100644 index 06b68feb..00000000 --- a/r2-streamer/src/main/java/org/readium/r2/streamer/server/handler/FontHandler.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Module: r2-streamer-kotlin - * Developers: Aferdita Muriqi, Clément Baumann - * - * Copyright (c) 2018. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.streamer.server.handler - - -import org.nanohttpd.protocols.http.IHTTPSession -import org.nanohttpd.protocols.http.response.IStatus -import org.nanohttpd.protocols.http.response.Response -import org.nanohttpd.protocols.http.response.Response.newFixedLengthResponse -import org.nanohttpd.protocols.http.response.Status -import org.nanohttpd.router.RouterNanoHTTPD -import org.readium.r2.shared.format.Format -import org.readium.r2.shared.format.MediaType -import org.readium.r2.streamer.BuildConfig.DEBUG -import org.readium.r2.streamer.server.Fonts -import timber.log.Timber -import java.io.InputStream - - -class FontHandler : RouterNanoHTTPD.DefaultHandler() { - - override fun getMimeType(): String? { - return null - } - - override fun getText(): String { - return ResponseStatus.FAILURE_RESPONSE - } - - override fun getStatus(): IStatus { - return Status.OK - } - - override fun get(uriResource: RouterNanoHTTPD.UriResource?, urlParams: Map?, session: IHTTPSession?): Response { - - val method = session!!.method - var uri = session.uri - - if (DEBUG) Timber.v("Method: $method, Url: $uri") - - return try { - val lastSlashIndex = uri.lastIndexOf('/') - uri = uri.substring(lastSlashIndex + 1, uri.length) - val resources = uriResource!!.initParameter(Fonts::class.java) - val resource = resources.get(uri) - val mediaType = Format.of(resource)?.mediaType ?: MediaType.BINARY - createResponse(Status.OK, mediaType.toString(), resource.inputStream()) - } catch (e: Exception) { - if (DEBUG) Timber.e( " Exception %s", e.toString()) - newFixedLengthResponse(Status.INTERNAL_ERROR, mimeType, ResponseStatus.FAILURE_RESPONSE) - } - } - - private fun createResponse(status: Status, mimeType: String, message: InputStream): Response { - val response = Response.newChunkedResponse(status, mimeType, message) - response.addHeader("Accept-Ranges", "bytes") - return response - } - -} diff --git a/r2-streamer/src/main/java/org/readium/r2/streamer/server/handler/JSHandler.kt b/r2-streamer/src/main/java/org/readium/r2/streamer/server/handler/JSHandler.kt deleted file mode 100644 index 8f7875ee..00000000 --- a/r2-streamer/src/main/java/org/readium/r2/streamer/server/handler/JSHandler.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Module: r2-streamer-kotlin - * Developers: Aferdita Muriqi, Clément Baumann - * - * Copyright (c) 2018. Readium Foundation. All rights reserved. - * Use of this source code is governed by a BSD-style license which is detailed in the - * LICENSE file present in the project repository where this source code is maintained. - */ - -package org.readium.r2.streamer.server.handler - -import org.nanohttpd.protocols.http.IHTTPSession -import org.nanohttpd.protocols.http.response.IStatus -import org.nanohttpd.protocols.http.response.Response -import org.nanohttpd.protocols.http.response.Response.newFixedLengthResponse -import org.nanohttpd.protocols.http.response.Status -import org.nanohttpd.router.RouterNanoHTTPD -import org.readium.r2.streamer.BuildConfig.DEBUG -import org.readium.r2.streamer.server.Resources -import timber.log.Timber - - -class JSHandler : RouterNanoHTTPD.DefaultHandler() { - - - override fun getMimeType(): String? { - return null - } - - override fun getText(): String { - return ResponseStatus.FAILURE_RESPONSE - } - - override fun getStatus(): IStatus { - return Status.OK - } - - override fun get(uriResource: RouterNanoHTTPD.UriResource?, urlParams: Map?, session: IHTTPSession?): Response { - - val method = session!!.method - var uri = session.uri - - if (DEBUG) Timber.v("Method: $method, Url: $uri") - - return try { - val lastSlashIndex = uri.lastIndexOf('/') - uri = uri.substring(lastSlashIndex + 1, uri.length) - val resources = uriResource!!.initParameter(Resources::class.java) - val x = createResponse(Status.OK, "text/javascript", resources.get(uri)) - x - } catch (e: Exception) { - if (DEBUG) Timber.e(" Exception %s", e.toString()) - newFixedLengthResponse(Status.INTERNAL_ERROR, mimeType, ResponseStatus.FAILURE_RESPONSE) - } - - } - - private fun createResponse(status: Status, mimeType: String, message: String): Response { - val response = newFixedLengthResponse(status, mimeType, message) - response.addHeader("Accept-Ranges", "bytes") - return response - } -} diff --git a/r2-streamer/src/main/java/org/readium/r2/streamer/server/handler/ManifestHandler.kt b/r2-streamer/src/main/java/org/readium/r2/streamer/server/handler/ManifestHandler.kt index c37d2e07..e094a94d 100644 --- a/r2-streamer/src/main/java/org/readium/r2/streamer/server/handler/ManifestHandler.kt +++ b/r2-streamer/src/main/java/org/readium/r2/streamer/server/handler/ManifestHandler.kt @@ -1,6 +1,6 @@ /* * Module: r2-streamer-kotlin - * Developers: Aferdita Muriqi, Clément Baumann + * Developers: Aferdita Muriqi, Clément Baumann, Mickaël Menu * * Copyright (c) 2018. Readium Foundation. All rights reserved. * Use of this source code is governed by a BSD-style license which is detailed in the @@ -9,41 +9,17 @@ package org.readium.r2.streamer.server.handler -import org.nanohttpd.protocols.http.IHTTPSession -import org.nanohttpd.protocols.http.response.IStatus +import android.net.Uri import org.nanohttpd.protocols.http.response.Response -import org.nanohttpd.protocols.http.response.Response.newFixedLengthResponse -import org.nanohttpd.protocols.http.response.Status import org.nanohttpd.router.RouterNanoHTTPD import org.readium.r2.shared.format.MediaType -import org.readium.r2.streamer.BuildConfig.DEBUG import org.readium.r2.streamer.fetcher.Fetcher -import timber.log.Timber -import java.io.IOException +internal class ManifestHandler : BaseHandler() { -class ManifestHandler : RouterNanoHTTPD.DefaultHandler() { - - override fun getMimeType(): String { - return MediaType.WEBPUB_MANIFEST.toString() - } - - override fun getText(): String { - return ResponseStatus.FAILURE_RESPONSE + override fun handle(resource: RouterNanoHTTPD.UriResource, uri: Uri, parameters: Map?): Response { + val fetcher = resource.initParameter(Fetcher::class.java) + return createResponse(mediaType = MediaType.WEBPUB_MANIFEST, body = fetcher.publication.manifest.toByteArray()) } - override fun getStatus(): IStatus { - return Status.OK - } - - override fun get(uriResource: RouterNanoHTTPD.UriResource?, urlParams: Map?, session: IHTTPSession?): Response { - return try { - val fetcher = uriResource!!.initParameter(Fetcher::class.java) - newFixedLengthResponse(status, mimeType, fetcher.publication.manifest) - } catch (e: IOException) { - if (DEBUG) Timber.v(" IOException %s", e.toString()) - newFixedLengthResponse(Status.INTERNAL_ERROR, mimeType, ResponseStatus.FAILURE_RESPONSE) - } - - } } diff --git a/r2-streamer/src/main/java/org/readium/r2/streamer/server/handler/PublicationResourceHandler.kt b/r2-streamer/src/main/java/org/readium/r2/streamer/server/handler/PublicationResourceHandler.kt new file mode 100644 index 00000000..64f69e9c --- /dev/null +++ b/r2-streamer/src/main/java/org/readium/r2/streamer/server/handler/PublicationResourceHandler.kt @@ -0,0 +1,158 @@ +/* + * Module: r2-streamer-kotlin + * Developers: Aferdita Muriqi, Clément Baumann + * + * Copyright (c) 2018. Readium Foundation. All rights reserved. + * Use of this source code is governed by a BSD-style license which is detailed in the + * LICENSE file present in the project repository where this source code is maintained. + */ + +package org.readium.r2.streamer.server.handler + +import org.nanohttpd.protocols.http.IHTTPSession +import org.nanohttpd.protocols.http.NanoHTTPD.MIME_PLAINTEXT +import org.nanohttpd.protocols.http.response.IStatus +import org.nanohttpd.protocols.http.response.Response +import org.nanohttpd.protocols.http.response.Response.newChunkedResponse +import org.nanohttpd.protocols.http.response.Response.newFixedLengthResponse +import org.nanohttpd.protocols.http.response.Status +import org.nanohttpd.router.RouterNanoHTTPD +import org.readium.r2.shared.format.MediaType +import org.readium.r2.streamer.BuildConfig.DEBUG +import org.readium.r2.streamer.fetcher.Fetcher +import timber.log.Timber +import java.io.IOException +import java.io.InputStream + + +class PublicationResourceHandler : RouterNanoHTTPD.DefaultHandler() { + + override fun getMimeType(): String? { + return null + } + + override fun getText(): String { + return ResponseStatus.FAILURE_RESPONSE + } + + override fun getStatus(): IStatus { + return Status.OK + } + + override fun get(uriResource: RouterNanoHTTPD.UriResource?, urlParams: Map?, + session: IHTTPSession?): Response? { + try { + if (DEBUG) Timber.v("Method: ${session!!.method}, Uri: ${session.uri}") + val fetcher = uriResource!!.initParameter(Fetcher::class.java) + + val filePath = getHref(session!!.uri) + val link = fetcher.publication.linkWithHref(filePath)!! + val mediaType = link.mediaType ?: MediaType.BINARY + + // If the content is of type html return the response this is done to + // skip the check for following font deobfuscation check + if (mediaType.isHtml) { + return serveResponse(session, fetcher.dataStream(filePath), mediaType.toString()) + } + + // ******************** + // FONT DEOBFUSCATION + // ******************** + + return serveResponse(session, fetcher.dataStream(filePath), mediaType.toString()) + } catch (e: Exception) { + if (DEBUG) Timber.e(e) + return newFixedLengthResponse(Status.INTERNAL_ERROR, mimeType, ResponseStatus.FAILURE_RESPONSE) + } + } + + private fun serveResponse(session: IHTTPSession, inputStream: InputStream, mimeType: String): Response { + var response: Response? + var rangeRequest: String? = session.headers["range"] + + try { + // Calculate etag + val etag = Integer.toHexString(inputStream.hashCode()) + + // Support skipping: + var startFrom: Long = 0 + var endAt: Long = -1 + if (rangeRequest != null) { + if (rangeRequest.startsWith("bytes=")) { + rangeRequest = rangeRequest.substring("bytes=".length) + val minus = rangeRequest.indexOf('-') + try { + if (minus > 0) { + startFrom = java.lang.Long.parseLong(rangeRequest.substring(0, minus)) + endAt = java.lang.Long.parseLong(rangeRequest.substring(minus + 1)) + } + } catch (ignored: NumberFormatException) { + } + + } + } + + // Change return code and add Content-Range header when skipping is requested + val streamLength = inputStream.available().toLong() + if (rangeRequest != null && startFrom >= 0) { + if (startFrom >= streamLength) { + response = createResponse(Status.RANGE_NOT_SATISFIABLE, MIME_PLAINTEXT, "") + response.addHeader("Content-Range", "bytes 0-0/$streamLength") + response.addHeader("ETag", etag) + } else { + if (endAt < 0) { + endAt = streamLength - 1 + } + var newLen = endAt - startFrom + 1 + if (newLen < 0) { + newLen = 0 + } + + val dataLen = newLen + inputStream.skip(startFrom) + + response = createResponse(Status.PARTIAL_CONTENT, mimeType, inputStream) + response.addHeader("Content-Length", "" + dataLen) + response.addHeader("Content-Range", "bytes $startFrom-$endAt/$streamLength") + response.addHeader("ETag", etag) + } + } else { + if (etag == session.headers["if-none-match"]) + response = createResponse(Status.NOT_MODIFIED, mimeType, "") + else { + response = createResponse(Status.OK, mimeType, inputStream) + response.addHeader("Content-Length", "" + streamLength) + response.addHeader("ETag", etag) + } + } + } catch (ioe: IOException) { + response = getResponse("Forbidden: Reading file failed") + } catch (ioe: NullPointerException) { + response = getResponse("Forbidden: Reading file failed") + } + + return response ?: getResponse("Error 404: File not found") + } + + private fun createResponse(status: Status, mimeType: String, message: InputStream): Response { + val response = newChunkedResponse(status, mimeType, message) + response.addHeader("Accept-Ranges", "bytes") + return response + } + + private fun createResponse(status: Status, mimeType: String, message: String): Response { + val response = newFixedLengthResponse(status, mimeType, message) + response.addHeader("Accept-Ranges", "bytes") + return response + } + + private fun getResponse(message: String): Response { + return createResponse(Status.OK, "text/plain", message) + } + + private fun getHref(path: String): String { + val offset = path.indexOf("/", 0) + val startIndex = path.indexOf("/", offset + 1) + return path.substring(startIndex + 1) + } +} \ No newline at end of file diff --git a/r2-streamer/src/main/java/org/readium/r2/streamer/server/handler/ResourceHandler.kt b/r2-streamer/src/main/java/org/readium/r2/streamer/server/handler/ResourceHandler.kt index 8e35f778..a49804bc 100644 --- a/r2-streamer/src/main/java/org/readium/r2/streamer/server/handler/ResourceHandler.kt +++ b/r2-streamer/src/main/java/org/readium/r2/streamer/server/handler/ResourceHandler.kt @@ -1,158 +1,35 @@ /* * Module: r2-streamer-kotlin - * Developers: Aferdita Muriqi, Clément Baumann + * Developers: Mickaël Menu * - * Copyright (c) 2018. Readium Foundation. All rights reserved. + * Copyright (c) 2020. Readium Foundation. All rights reserved. * Use of this source code is governed by a BSD-style license which is detailed in the * LICENSE file present in the project repository where this source code is maintained. */ package org.readium.r2.streamer.server.handler -import org.nanohttpd.protocols.http.IHTTPSession -import org.nanohttpd.protocols.http.NanoHTTPD.MIME_PLAINTEXT -import org.nanohttpd.protocols.http.response.IStatus +import android.net.Uri import org.nanohttpd.protocols.http.response.Response -import org.nanohttpd.protocols.http.response.Response.newChunkedResponse -import org.nanohttpd.protocols.http.response.Response.newFixedLengthResponse -import org.nanohttpd.protocols.http.response.Status import org.nanohttpd.router.RouterNanoHTTPD +import org.readium.r2.shared.format.Format import org.readium.r2.shared.format.MediaType -import org.readium.r2.streamer.BuildConfig.DEBUG -import org.readium.r2.streamer.fetcher.Fetcher -import timber.log.Timber -import java.io.IOException -import java.io.InputStream +import org.readium.r2.streamer.server.Resources -class ResourceHandler : RouterNanoHTTPD.DefaultHandler() { - - override fun getMimeType(): String? { - return null - } - - override fun getText(): String { - return ResponseStatus.FAILURE_RESPONSE - } - - override fun getStatus(): IStatus { - return Status.OK - } - - override fun get(uriResource: RouterNanoHTTPD.UriResource?, urlParams: Map?, - session: IHTTPSession?): Response? { - try { - if (DEBUG) Timber.v("Method: ${session!!.method}, Uri: ${session.uri}") - val fetcher = uriResource!!.initParameter(Fetcher::class.java) - - val filePath = getHref(session!!.uri) - val link = fetcher.publication.linkWithHref(filePath)!! - val mediaType = link.mediaType ?: MediaType.BINARY - - // If the content is of type html return the response this is done to - // skip the check for following font deobfuscation check - if (mediaType.isHtml) { - return serveResponse(session, fetcher.dataStream(filePath), mediaType.toString()) - } - - // ******************** - // FONT DEOBFUSCATION - // ******************** - - return serveResponse(session, fetcher.dataStream(filePath), mediaType.toString()) - } catch (e: Exception) { - if (DEBUG) Timber.e(e) - return newFixedLengthResponse(Status.INTERNAL_ERROR, mimeType, ResponseStatus.FAILURE_RESPONSE) - } - } - - private fun serveResponse(session: IHTTPSession, inputStream: InputStream, mimeType: String): Response { - var response: Response? - var rangeRequest: String? = session.headers["range"] - - try { - // Calculate etag - val etag = Integer.toHexString(inputStream.hashCode()) - - // Support skipping: - var startFrom: Long = 0 - var endAt: Long = -1 - if (rangeRequest != null) { - if (rangeRequest.startsWith("bytes=")) { - rangeRequest = rangeRequest.substring("bytes=".length) - val minus = rangeRequest.indexOf('-') - try { - if (minus > 0) { - startFrom = java.lang.Long.parseLong(rangeRequest.substring(0, minus)) - endAt = java.lang.Long.parseLong(rangeRequest.substring(minus + 1)) - } - } catch (ignored: NumberFormatException) { - } - - } - } - - // Change return code and add Content-Range header when skipping is requested - val streamLength = inputStream.available().toLong() - if (rangeRequest != null && startFrom >= 0) { - if (startFrom >= streamLength) { - response = createResponse(Status.RANGE_NOT_SATISFIABLE, MIME_PLAINTEXT, "") - response.addHeader("Content-Range", "bytes 0-0/$streamLength") - response.addHeader("ETag", etag) - } else { - if (endAt < 0) { - endAt = streamLength - 1 - } - var newLen = endAt - startFrom + 1 - if (newLen < 0) { - newLen = 0 - } - - val dataLen = newLen - inputStream.skip(startFrom) - - response = createResponse(Status.PARTIAL_CONTENT, mimeType, inputStream) - response.addHeader("Content-Length", "" + dataLen) - response.addHeader("Content-Range", "bytes $startFrom-$endAt/$streamLength") - response.addHeader("ETag", etag) - } - } else { - if (etag == session.headers["if-none-match"]) - response = createResponse(Status.NOT_MODIFIED, mimeType, "") - else { - response = createResponse(Status.OK, mimeType, inputStream) - response.addHeader("Content-Length", "" + streamLength) - response.addHeader("ETag", etag) - } - } - } catch (ioe: IOException) { - response = getResponse("Forbidden: Reading file failed") - } catch (ioe: NullPointerException) { - response = getResponse("Forbidden: Reading file failed") - } - - return response ?: getResponse("Error 404: File not found") - } - - private fun createResponse(status: Status, mimeType: String, message: InputStream): Response { - val response = newChunkedResponse(status, mimeType, message) - response.addHeader("Accept-Ranges", "bytes") - return response - } - - private fun createResponse(status: Status, mimeType: String, message: String): Response { - val response = newFixedLengthResponse(status, mimeType, message) - response.addHeader("Accept-Ranges", "bytes") - return response - } +/** + * Serves in-memory resources. + * + * The NanoHTTPD init parameter must be an instance of `Resources`. + */ +internal class ResourceHandler : BaseHandler() { - private fun getResponse(message: String): Response { - return createResponse(Status.OK, "text/plain", message) + override fun handle(resource: RouterNanoHTTPD.UriResource, uri: Uri, parameters: Map?): Response { + val resources = resource.initParameter(Resources::class.java) + val href = uri.path?.substringAfterLast("/") ?: return notFoundResponse + val body = resources.get(href) ?: return notFoundResponse + val format = Format.of(fileExtension = href.substringAfterLast(".", "")) + return createResponse(mediaType = format?.mediaType ?: MediaType.BINARY, body = body) } - private fun getHref(path: String): String { - val offset = path.indexOf("/", 0) - val startIndex = path.indexOf("/", offset + 1) - return path.substring(startIndex + 1) - } -} \ No newline at end of file +}