Skip to content

Commit

Permalink
Closes mozilla-mobile#6314: Support fetching / downloading data URIs
Browse files Browse the repository at this point in the history
  • Loading branch information
csadilek committed Mar 31, 2020
1 parent ca13af0 commit 79a41db
Show file tree
Hide file tree
Showing 11 changed files with 100 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,10 @@ class GeckoViewFetchTestCases : mozilla.components.tooling.fetch.tests.FetchTest
override fun getThrowsIOExceptionWhenHostNotReachable() {
super.getThrowsIOExceptionWhenHostNotReachable()
}

@Test
@UiThreadTest
override fun getDataUri() {
super.getDataUri()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import mozilla.components.concept.fetch.Headers
import mozilla.components.concept.fetch.MutableHeaders
import mozilla.components.concept.fetch.Request
import mozilla.components.concept.fetch.Response
import mozilla.components.concept.fetch.fetchDataUri
import mozilla.components.concept.fetch.isDataUri

import org.mozilla.geckoview.GeckoRuntime
import org.mozilla.geckoview.GeckoWebExecutor
Expand Down Expand Up @@ -39,6 +41,10 @@ class GeckoViewFetchClient(

@Throws(IOException::class)
override fun fetch(request: Request): Response {
if (request.isDataUri()) {
return fetchDataUri(request)
}

val webRequest = request.toWebRequest(defaultHeaders)

val readTimeOut = request.readTimeout ?: maxReadTimeOut
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

package mozilla.components.concept.fetch

import android.util.Base64
import mozilla.components.concept.fetch.Response.Companion.CONTENT_LENGTH_HEADER
import mozilla.components.concept.fetch.Response.Companion.CONTENT_TYPE_HEADER
import java.io.ByteArrayInputStream
import java.io.IOException

/**
Expand Down Expand Up @@ -62,3 +66,31 @@ abstract class Client {
"Connection" to "keep-alive"
)
}

/**
* "Fetches" a base64 encoded data URI.
*
* @param request The [Request] for the data URI.
* @return The generated [Response] including the decoded bytes as body.
*/
@Suppress("TooGenericExceptionCaught")
fun fetchDataUri(request: Request): Response {
return try {
val dataUri = request.url
val contentType = dataUri.substringAfter("data:").substringBefore(";base64")

val bytes = Base64.decode(dataUri.substring(dataUri.lastIndexOf(',') + 1), Base64.DEFAULT)
val headers = MutableHeaders(
CONTENT_TYPE_HEADER to contentType,
CONTENT_LENGTH_HEADER to bytes.size.toString()
)
Response(
dataUri,
Response.SUCCESS,
headers,
Response.Body(ByteArrayInputStream(bytes), contentType)
)
} catch (e: Exception) {
throw IOException("Failed to decode data URI")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,8 @@ data class Request(
OMIT
}
}

/**
* Checks whether or not the request is for a data URI.
*/
fun Request.isDataUri() = url.startsWith("data:")
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ data class Response(
companion object {
val SUCCESS_STATUS_RANGE = 200..299
val CLIENT_ERROR_STATUS_RANGE = 400..499
const val SUCCESS = 200
const val CONTENT_TYPE_HEADER = "Content-Type"
const val CONTENT_LENGTH_HEADER = "Content-Length"
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ class FetchDownloadManager<T : AbstractFetchDownloadService>(
* @return the id reference of the scheduled download.
*/
override fun download(download: DownloadState, cookie: String): Long? {
if (!download.isScheme(listOf("http", "https"))) {
// We are ignoring everything that is not http or https. This is a limitation of
if (!download.isScheme(listOf("http", "https", "data"))) {
// We are ignoring everything that is not http(s) or data. This is a limitation of
// GeckoView: https://bugzilla.mozilla.org/show_bug.cgi?id=1501735 and
// https://bugzilla.mozilla.org/show_bug.cgi?id=1432949
return null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import mozilla.components.concept.fetch.Headers
import mozilla.components.concept.fetch.MutableHeaders
import mozilla.components.concept.fetch.Request
import mozilla.components.concept.fetch.Response
import mozilla.components.concept.fetch.fetchDataUri
import mozilla.components.concept.fetch.isDataUri
import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient.Companion.getOrCreateCookieManager
import java.io.FileNotFoundException
import java.io.IOException
Expand All @@ -25,6 +27,10 @@ import java.util.zip.GZIPInputStream
class HttpURLConnectionClient : Client() {
@Throws(IOException::class)
override fun fetch(request: Request): Response {
if (request.isDataUri()) {
return fetchDataUri(request)
}

val connection = (URL(request.url).openConnection() as HttpURLConnection)

connection.setupWith(request)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ package mozilla.components.lib.fetch.httpurlconnection

import mozilla.components.concept.fetch.Client
import mozilla.components.concept.fetch.Request
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.junit.Test
import org.mockito.Mockito.spy
import java.io.IOException
import java.net.HttpURLConnection
import java.net.URL

Expand All @@ -34,4 +38,18 @@ class HttpUrlConnectionFetchTestCases : mozilla.components.tooling.fetch.tests.F
connection.setupWith((Request("https://mozilla.org", useCaches = false)))
assertFalse(connection.useCaches)
}

@Test
override fun getDataUri() {
// We can't run this test case because Base64 encoding is not available but we can verify
// we attempt to fetch the data URI
val client = spy(createNewClient())
val request = Request(url = "data:text/plain;charset=utf-8;base64,SGVsbG8sIFdvcmxkIQ==")
try {
client.fetch(request)
fail("Expected attempt to decode data URI")
} catch (e: IOException) {
assertEquals("Failed to decode data URI", e.message)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
mock-maker-inline
// This allows mocking final classes (classes are final by default in Kotlin)
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import mozilla.components.concept.fetch.Headers
import mozilla.components.concept.fetch.MutableHeaders
import mozilla.components.concept.fetch.Request
import mozilla.components.concept.fetch.Response
import mozilla.components.concept.fetch.fetchDataUri
import mozilla.components.concept.fetch.isDataUri
import mozilla.components.lib.fetch.okhttp.OkHttpClient.Companion.CACHE_MAX_SIZE
import mozilla.components.lib.fetch.okhttp.OkHttpClient.Companion.getOrCreateCookieManager
import okhttp3.Cache
Expand All @@ -30,6 +32,10 @@ class OkHttpClient(
private val context: Context? = null
) : Client() {
override fun fetch(request: Request): Response {
if (request.isDataUri()) {
return fetchDataUri(request)
}

val requestClient = client.rebuildFor(request, context)

val requestBuilder = createRequestBuilderWithBody(request)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,20 @@ abstract class FetchTestCases {
}
}

@Test
open fun getDataUri() {
val client = createNewClient()
val response = client.fetch(Request(url = "data:text/plain;charset=utf-8;base64,SGVsbG8sIFdvcmxkIQ=="))
assertEquals("13", response.headers["Content-Length"])
assertEquals("text/plain;charset=utf-8", response.headers["Content-Type"])
assertEquals("Hello, World!", response.body.string())

val responseNoCharset = client.fetch(Request(url = "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ=="))
assertEquals("13", responseNoCharset.headers["Content-Length"])
assertEquals("text/plain", responseNoCharset.headers["Content-Type"])
assertEquals("Hello, World!", responseNoCharset.body.string())
}

private inline fun withServerResponding(
vararg responses: MockResponse,
crossinline block: MockWebServer.(Client) -> Unit
Expand Down

0 comments on commit 79a41db

Please sign in to comment.