mirror of
https://github.com/stablyai/orca.git
synced 2026-10-01 08:01:56 +00:00
fix(mobile-web-shell): make the Android module compile and hold its stage boundary
The Kotlin sources for the hybrid WebView shell had never been built by any workflow, so five compile errors and one boundary escape shipped unnoticed: - MobileWebShellView shadowed ExpoView.appContext and used `return` inside three expression-bodied functions. - MobileWebBridgeDocumentUrl passed a java.net.URI to the android.net.Uri origin check; it now compares against an Android-free host projection so the JVM unit test can exercise it. - beginStage called mkdirs() before checking its ancestors, creating the stage through a symlinked `staging` directory and leaving it behind when the later check rejected it (MobileWebCacheWriteBoundaryTest). 43 tests now pass under :orca-expo-mobile-web-shell:testDebugUnitTest. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
+1
-1
@@ -9,7 +9,7 @@ internal fun isAllowedMobileWebBridgeDocumentUrl(value: String, sessionId: Strin
|
||||
val url = URI(value)
|
||||
value.length <= MOBILE_WEB_DOCUMENT_URL_LIMIT &&
|
||||
url.scheme == MOBILE_WEB_ORIGIN_SCHEME &&
|
||||
isMobileWebOriginForSession(url, sessionId) &&
|
||||
url.host == mobileWebOriginHostForSession(sessionId) &&
|
||||
url.port == -1 &&
|
||||
url.userInfo == null &&
|
||||
url.fragment == sessionId
|
||||
|
||||
+4
@@ -14,6 +14,10 @@ internal fun mobileWebOriginForSession(sessionId: String): String {
|
||||
|
||||
internal fun mobileWebOriginUriForSession(sessionId: String): Uri = Uri.parse(mobileWebOriginForSession(sessionId))
|
||||
|
||||
/** Android-free host projection so JVM unit tests can check origins without android.net.Uri. */
|
||||
internal fun mobileWebOriginHostForSession(sessionId: String): String =
|
||||
mobileWebOriginForSession(sessionId).substringAfter("://")
|
||||
|
||||
internal fun isMobileWebOriginForSession(url: Uri, sessionId: String): Boolean =
|
||||
url.scheme == MOBILE_WEB_ORIGIN_SCHEME &&
|
||||
url.host == mobileWebOriginUriForSession(sessionId).host &&
|
||||
|
||||
+7
-1
@@ -103,8 +103,14 @@ internal class MobileWebPackageStore internal constructor(
|
||||
manifestJson.toByteArray(Charsets.UTF_8).size +
|
||||
canonicalManifestJson.toByteArray(Charsets.UTF_8).size
|
||||
reserveCacheCapacity(hostKey, reservedByteLength)
|
||||
val stageRoot = File(cacheRoot, "$hostKey/staging/$stageId")
|
||||
val hostRoot = File(cacheRoot, hostKey)
|
||||
val stagingRoot = File(hostRoot, "staging")
|
||||
val stageRoot = File(stagingRoot, stageId)
|
||||
try {
|
||||
// mkdirs() would create the stage *through* a symlinked ancestor before any later check
|
||||
// could reject it, so the ancestors are validated first.
|
||||
require(isMobileWebUnlinkedPath(hostRoot, cacheRoot)) { "mobile_web_stage_create_failed" }
|
||||
require(isMobileWebUnlinkedPath(stagingRoot, cacheRoot)) { "mobile_web_stage_create_failed" }
|
||||
require(stageRoot.mkdirs()) { "mobile_web_stage_create_failed" }
|
||||
require(isMobileWebUnlinkedPath(stageRoot, cacheRoot)) {
|
||||
"mobile_web_stage_create_failed"
|
||||
|
||||
+20
-17
@@ -64,7 +64,7 @@ private val MOBILE_WEB_MERMAID_FRAME_CSP = listOf(
|
||||
@SuppressLint("ViewConstructor", "SetJavaScriptEnabled")
|
||||
internal class MobileWebShellView(
|
||||
context: Context,
|
||||
private val appContext: AppContext
|
||||
appContext: AppContext
|
||||
) : ExpoView(context, appContext) {
|
||||
private val onBridgeMessage by EventDispatcher<Map<String, Any>>()
|
||||
private val onNavigationBlocked by EventDispatcher<Map<String, Any>>()
|
||||
@@ -345,32 +345,35 @@ internal class MobileWebShellView(
|
||||
)
|
||||
}
|
||||
|
||||
private fun isAllowedDocumentUrl(url: Uri): Boolean =
|
||||
activeSessionId != null &&
|
||||
isMobileWebOriginForSession(url, activeSessionId ?: return false) &&
|
||||
private fun isAllowedDocumentUrl(url: Uri): Boolean {
|
||||
val sessionId = activeSessionId ?: return false
|
||||
return isMobileWebOriginForSession(url, sessionId) &&
|
||||
url.path == "/" &&
|
||||
url.encodedPath == "/" &&
|
||||
url.query == null &&
|
||||
url.fragment == activeSessionId &&
|
||||
url.toString().length <= 8 * 1024
|
||||
url.fragment == sessionId &&
|
||||
url.toString().length <= 8 * 1024
|
||||
}
|
||||
|
||||
private fun isAllowedDocumentRequestUrl(url: Uri): Boolean =
|
||||
activeSessionId != null &&
|
||||
isMobileWebOriginForSession(url, activeSessionId ?: return false) &&
|
||||
url.path == "/" &&
|
||||
url.encodedPath == "/" &&
|
||||
url.query == null &&
|
||||
(url.fragment == null || url.fragment == activeSessionId) &&
|
||||
url.toString().length <= 8 * 1024
|
||||
private fun isAllowedDocumentRequestUrl(url: Uri): Boolean {
|
||||
val sessionId = activeSessionId ?: return false
|
||||
return isMobileWebOriginForSession(url, sessionId) &&
|
||||
url.path == "/" &&
|
||||
url.encodedPath == "/" &&
|
||||
url.query == null &&
|
||||
(url.fragment == null || url.fragment == sessionId) &&
|
||||
url.toString().length <= 8 * 1024
|
||||
}
|
||||
|
||||
private fun isAllowedEmbeddedDocumentUrl(url: Uri): Boolean =
|
||||
activeSessionId != null &&
|
||||
isMobileWebOriginForSession(url, activeSessionId ?: return false) &&
|
||||
private fun isAllowedEmbeddedDocumentUrl(url: Uri): Boolean {
|
||||
val sessionId = activeSessionId ?: return false
|
||||
return isMobileWebOriginForSession(url, sessionId) &&
|
||||
url.path == "/$MOBILE_WEB_MERMAID_FRAME_PATH" &&
|
||||
url.encodedPath == "/$MOBILE_WEB_MERMAID_FRAME_PATH" &&
|
||||
url.query == null &&
|
||||
url.fragment == null &&
|
||||
url.toString().length <= 8 * 1024
|
||||
}
|
||||
|
||||
private fun blockedResponse(): WebResourceResponse = WebResourceResponse(
|
||||
"text/plain",
|
||||
|
||||
Reference in New Issue
Block a user