package org.meshbay.client.bridge import android.net.Uri import android.os.Handler import android.os.Looper import android.util.Log import android.webkit.WebView import androidx.webkit.JavaScriptReplyProxy import androidx.webkit.WebMessageCompat import androidx.webkit.WebViewCompat import org.json.JSONArray import org.json.JSONObject import org.meshbay.client.save.BinaryFrame import org.meshbay.client.shell.UiAssets import java.util.concurrent.Executors /** * Everything the interface may ask of the application, and the only way in. * * `addWebMessageListener` injects `meshbayNative` only into documents of the * packaged origin; the shim (assets/bridge/meshbay-bridge.js) takes it at * document start and hides it. But a same-origin child frame gets one too — the * spike measured it — so what actually confines the bridge is the check here: * **the packaged origin's top-level document, and nothing else** (main.js * `fromOurPage`). The page parses decrypted content from nodes, which is * attacker-controlled input, so every argument is checked again in Channels. */ class Bridge(private val channels: Channels) : WebViewCompat.WebMessageListener { private val main = Handler(Looper.getMainLooper()) // Hub calls and key operations block; none may run on the UI thread. private val work = Executors.newCachedThreadPool() private val serial = Executors.newSingleThreadExecutor() override fun onPostMessage(view: WebView, message: WebMessageCompat, sourceOrigin: Uri, isMainFrame: Boolean, replyProxy: JavaScriptReplyProxy) { if (!isMainFrame || sourceOrigin.toString() != UiAssets.ORIGIN) { Log.w(TAG, "refused a message from $sourceOrigin (main frame: $isMainFrame)") val id = if (message.type == WebMessageCompat.TYPE_STRING) try { JSONObject(message.data ?: "").optLong("id", -1) } catch (e: Exception) { -1 } else -1 replyProxy.postMessage(error(id, "Refused: not the MeshBay interface")) return } if (message.type == WebMessageCompat.TYPE_ARRAY_BUFFER) { val frame = BinaryFrame.parse(message.arrayBuffer) ?: return dispatch(frame.id, replyProxy, "binary ${frame.channel}", channels.ordered(frame)) { channels.binary(frame) } return } val request = try { JSONObject(message.data ?: return) } catch (e: Exception) { return } val id = request.optLong("id", -1) val channel = request.optString("ch") val args = request.optJSONArray("args") ?: JSONArray() dispatch(id, replyProxy, channel, channels.ordered(channel)) { channels.call(channel, args) } } private fun dispatch(id: Long, replyProxy: JavaScriptReplyProxy, channel: String, ordered: Boolean, call: () -> Any?) { (if (ordered) serial else work).execute { val reply = try { JSONObject().put("id", id).put("ok", true).put("value", call() ?: JSONObject.NULL).toString() } catch (e: Refused) { error(id, e.message ?: "Refused") } catch (e: Exception) { Log.w(TAG, "$channel failed", e) error(id, e.message ?: e.javaClass.simpleName) } main.post { replyProxy.postMessage(reply) } } } private fun error(id: Long, message: String) = JSONObject().put("id", id).put("ok", false).put("error", message).toString() companion object { const val TAG = "MeshBay" const val PORT = "meshbayNative" } }