Skip to content

Commit

Permalink
Upgraded to Android 34 (#53)
Browse files Browse the repository at this point in the history
  • Loading branch information
smikhalevski authored Feb 1, 2024
1 parent b1c0890 commit 5470e1a
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 41 deletions.
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -418,15 +418,15 @@ dependencies {
}
```

2. Load the static assets from a directory when a particular URL is requested in the WebView:
2. Load the static assets from a directory on the device when a particular URL is requested in the WebView:

```kotlin
import androidx.webkit.WebViewAssetLoader
import org.racehorse.AssetLoaderPlugin
import org.racehorse.StaticPathHandler

EventBus.getDefault().register(
AssetLoaderPlugin(activity).also {
AssetLoaderPlugin(activity).apply {
registerAssetLoader(
"https://example.com",
StaticPathHandler(File(activity.filesDir, "www"))
Expand All @@ -437,12 +437,14 @@ EventBus.getDefault().register(
webView.loadUrl("https://example.com")
```

During development, if you're running a watcher server on localhost, use `LocalhostDevPathHandler` to serve its content
to the webview:
During development, if you're running a server on localhost, use `ProxyPathHandler` to serve contents to the webview:

```kotlin
AssetLoaderPlugin(activity).also {
registerAssetLoader("https://example.com", LocalhostDevPathHandler(8080))
AssetLoaderPlugin(activity).apply {
registerAssetLoader(
"https://example.com",
ProxyPathHandler("http://10.0.2.2:10001")
)
}
```

Expand All @@ -457,7 +459,7 @@ window.location.href = 'https://google.com'
To disable this behaviour:

```kotlin
AssetLoaderPlugin(activity).also {
AssetLoaderPlugin(activity).apply {
isUnhandledRequestOpenedInExternalBrowser = false
}
```
Expand Down
4 changes: 2 additions & 2 deletions android/example/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,10 @@ dependencies {
implementation("com.google.code.gson:gson:2.10.1")

// ActivityPlugin
implementation("androidx.lifecycle:lifecycle-process:2.6.2")
implementation("androidx.lifecycle:lifecycle-process:2.7.0")

// AssetLoaderPlugin
implementation("androidx.webkit:webkit:1.9.0")
implementation("androidx.webkit:webkit:1.10.0")

// DevicePlugin
implementation("androidx.appcompat:appcompat:1.6.1")
Expand Down
2 changes: 1 addition & 1 deletion android/example/src/main/java/com/example/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ class MainActivity : AppCompatActivity() {
// 1️⃣ Live reload

// Rollup starts server on localhost:10001
assetLoaderPlugin.registerAssetLoader("https://example.com", LocalhostDevPathHandler(10001))
assetLoaderPlugin.registerAssetLoader("https://example.com", ProxyPathHandler("http://10.0.2.2:10001"))

// Example app uses livereload that is loaded from http://10.0.2.2:35729, since the app is rendered using
// https://example.com which uses HTTPS, live reload is rejected because of the mixed content policy.
Expand Down
6 changes: 3 additions & 3 deletions android/racehorse/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ tasks.dokkaHtml.configure {

android {
namespace = "org.racehorse"
compileSdk = 33
compileSdk = 34

defaultConfig {
// https://apilevels.com/
Expand Down Expand Up @@ -80,10 +80,10 @@ dependencies {
compileOnly("com.google.code.gson:gson:2.10.1")

// ActivityPlugin
compileOnly("androidx.lifecycle:lifecycle-process:2.6.2")
compileOnly("androidx.lifecycle:lifecycle-process:2.7.0")

// AssetLoaderPlugin
compileOnly("androidx.webkit:webkit:1.9.0")
compileOnly("androidx.webkit:webkit:1.10.0")

// DevicePlugin
compileOnly("androidx.appcompat:appcompat:1.6.1")
Expand Down
62 changes: 44 additions & 18 deletions android/racehorse/src/main/java/org/racehorse/AssetLoaderPlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ import org.racehorse.webview.ShouldInterceptRequestEvent
import org.racehorse.webview.ShouldOverrideUrlLoadingEvent
import java.io.File
import java.io.FileInputStream
import java.io.IOException
import java.net.HttpURLConnection
import java.net.URL
import java.net.URLConnection
import java.util.TreeMap

/**
* Intercepts requests and serves the responses using an registered asset loaders.
Expand Down Expand Up @@ -84,12 +87,10 @@ open class AssetLoaderPlugin(private val activity: ComponentActivity) {
*
* @param baseDir The directory on the device from which files are served.
* @param indexFileName The name of the index file to look for if handled path is a directory.
* @param headers The map of headers returns with each response.
*/
open class StaticPathHandler(
private val baseDir: File,
private val indexFileName: String = "index.html",
private val headers: Map<String, String>? = null
private val indexFileName: String = "index.html"
) : PathHandler {

private val baseDirPath = baseDir.canonicalPath
Expand All @@ -104,7 +105,7 @@ open class StaticPathHandler(
}
if (file.isFile && file.canRead()) {
val mimeType = URLConnection.guessContentTypeFromName(path)
return WebResourceResponse(mimeType, null, 200, "OK", headers, FileInputStream(file))
return WebResourceResponse(mimeType, null, FileInputStream(file))
}
}

Expand All @@ -113,22 +114,47 @@ open class StaticPathHandler(
}

/**
* Localhost dev server path handler.
*
* @param port The port on the localhost where the dev server is started.
* @param pathPrefix The path prefix of the dev server.
* @param headers The map of headers returns with each response.
* Redirects content from the given URL.
*/
open class LocalhostDevPathHandler(
private val port: Int,
private val pathPrefix: String = "",
private val headers: Map<String, String>? = null
) : PathHandler {
override fun handle(path: String): WebResourceResponse? {
val connection = URL("http://10.0.2.2:$port$pathPrefix/$path").openConnection()
open class ProxyPathHandler(private val baseUrl: URL) : PathHandler {

constructor(baseUrl: String) : this(URL(baseUrl))

override fun handle(path: String): WebResourceResponse {
val connection = openConnection(path)

val headers = connection.headerFields
.toMutableMap()
.apply { remove(null) }
.mapValuesTo(TreeMap(String.CASE_INSENSITIVE_ORDER)) { it.value.joinToString("; ") }

headers.remove("Content-Type")
headers.remove("Content-Length")

val mimeType = connection.contentType?.substringBefore(';')

val inputStream = try {
connection.inputStream
} catch (_: IOException) {
null
}

return WebResourceResponse(
mimeType,
connection.contentEncoding,
connection.responseCode,
connection.responseMessage,
headers,
inputStream
)
}

open fun openConnection(path: String): HttpURLConnection {
val connection = URL(baseUrl, path).openConnection() as HttpURLConnection

connection.connectTimeout = 1000
connection.instanceFollowRedirects = true
connection.connectTimeout = 10_000

return WebResourceResponse(connection.contentType, null, 200, "OK", headers, connection.getInputStream())
return connection
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import java.io.FileOutputStream
import java.io.IOException
import java.net.HttpURLConnection
import java.net.URLConnection
import java.util.zip.ZipException
import java.util.zip.ZipInputStream

/**
Expand Down Expand Up @@ -97,7 +98,7 @@ internal class BundleDownload(
HttpURLConnection.HTTP_OK -> readLength = 0L
HttpURLConnection.HTTP_PARTIAL -> {}

else -> throw IOException("Cannot download bundle")
else -> throw IOException("Cannot download the bundle")
}

if (connection.getHeaderField("Accept-Ranges")?.contains("bytes") == true) {
Expand Down Expand Up @@ -126,18 +127,30 @@ internal class BundleDownload(
private fun unzip() {
val unzipDirPath = unzipDir.canonicalPath + File.separator

ZipInputStream(zipFile.inputStream()).use { inputStream ->
while (!isStopped) {
val file = File(unzipDir, inputStream.nextEntry?.name ?: break)
try {
ZipInputStream(zipFile.inputStream()).use { zipInputStream ->
while (!isStopped) {
val zipEntry = try {
zipInputStream.nextEntry ?: break
} catch (_: ZipException) {
// https://developer.android.com/about/versions/14/behavior-changes-14#zip-path-traversal
zipInputStream.closeEntry()
continue
}

// https://snyk.io/research/zip-slip-vulnerability
if (file.canonicalPath.startsWith(unzipDirPath)) {
file.parentFile!!.mkdirs()
inputStream.copyTo(FileOutputStream(file))
}
val file = File(unzipDir, zipEntry.name)

inputStream.closeEntry()
// https://snyk.io/research/zip-slip-vulnerability
if (file.canonicalPath.startsWith(unzipDirPath)) {
file.parentFile!!.mkdirs()
zipInputStream.copyTo(FileOutputStream(file))
}

zipInputStream.closeEntry()
}
}
} catch (e: Throwable) {
throw IllegalStateException("Cannot unzip the bundle", e)
}
}
}

0 comments on commit 5470e1a

Please sign in to comment.