aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/bridge/Bridge.kt
blob: 50552fa2ac74d0ad5a1541544747747c969d0099 (plain) (blame)
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"
    }
}