1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
|
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"
}
}
|