# 使用 Frida 导出接口

您可以通过此功能实现将 frida RPC 导出的方法作为 FIRERPA 接口扩展调用。您可以通过自行编写功能性 Hook 代码实现对 APP 控制的极致。此功能需要您熟悉 frida hook 脚本的编写，如果您已有现存脚本，您需要对您的脚本做稍微的改写，使用我们的特定方法来实现。

```{attention}
自 9.0 开始，我们内置使用的 frida 17.x 将需要自行打包 frida-java-bridge 进脚本，否则会出现 Java not defined 相关错误，这项改动属于 frida 官方的改动，依据官方的改动说明，您需要新建 nodejs 项目并引入 java bridge，详细请参考 https://github.com/oleavr/frida-agent-example 或者使用我们提供的 tools/frida_script_generate.py 对原来的 js 脚本进行二次封装。
```

## 编写导出脚本

您需要编写特定格式的脚本，导出函数名必须遵循命名规范，务必是首字母小写的 camelCase 驼峰命名。对于定义性的名称如 HTTP，方法命名不可使用全大写格式，如名称 `sendHTTPRequest`，应在导出脚本中写为 `sendHttpRequest` 而非使用全大写 HTTP，以下是一个脚本应遵循的大致结构。

```js
Java.perform(function() {
const String = Java.use("java.lang.String")
rpc.exports.exampleFunc1 =  function (a, b) {
        return performRpcJVMCall(function() {
                return String.$new("Execute on JVM thread:" + a + b).toString()
        })
}
rpc.exports.exampleFunc2 = function (a, b) {
        return performRpcJVMCallOnMain(function() {
                return String.$new("Execute on Main UI thread:" + a + b).toString()
        })
}
rpc.exports.exampleFunc3 = function (a, b) {
        return performRpcCall(function() {
                return a + b
        })
}})
```

在上面的示例脚本您可以看到三种方法定义，其中 `return performRpc` 模式的代码块是必须使用的，他可以确保您的值正确返回。这三种不同的 performRpc 调用代码块代表的意义分别为：

```js
return performRpcJVMCall(function() {
        // Execute on JVM thread
})
```

代码块 performRpcJVMCall 代表在 JVM 中执行您的代码，您可以在块里使用 JVM 相关功能，例如 Java.use 或者其他涉及到应用 Java 层的操作等。

```js
return performRpcJVMCallOnMain(function() {
        // Execute on UI thread
})
```

代码块 performRpcJVMCallOnMain，代表在 JVM 中并且在 UI 主线程中执行您的代码，对于涉及到 UI 或者主线程的操作，您需要在这个代码块里执行才能成功，同时需要注意您的代码不会阻塞主线程执行，否则可能会导致应用无响应甚至崩溃。

```js
return performRpcCall(function() {
        // Execute Normal JS code
})
```

代码块 performRpcCall，您只能在此执行一些基础的 js 代码，不可在此使用 Java 层的逻辑。

```{attention}
您可能仍然需要使用 Java.perform 来封装整体代码块来确保 Java 逻辑正常执行。
```

现在您已经了解了我们代码的基本编写逻辑，您可以将您的代码封装成这种格式的来适配，如果只是想测试，也可以直接复制这段代码来开始体验，我们下面也是利用此 Hook 代码进行讲解。

## 注入导出脚本

这里的注入导出脚本的方法和 `持久化 Frida 脚本` 章节中的其实是完全一样的使用方法。

```python
app = d.application("com.android.settings")
app.attach_script(script, runtime=ScriptRuntime.RUNTIME_QJS, standup=5)
```

调用了注入接口后，您的脚本也应该就注入了，您可以执行如下调用来检查脚本是否正常。

```python
app.is_script_alive()
```

## 调用导出方法

调用脚本内的导出方法的方式和您原生使用 FIRERPA 接口是很相似的，请看示例代码。

```python
app = d.application("com.android.settings")
app.exampleFunc1("FIRE", "RPA")
```

对于多开应用，您需要获取多开应用的实例

```python
app = d.application("com.android.settings", user=UID)
app.exampleFunc1("FIRE", "RPA")
```

以上的调用方法等价于下面的，您可以发现方法中的 `Func1` 变成了 `_func1`，这两种都是可以的。

```python
app.example_func1("FIRE", "RPA")
```

```python
>>> app.example_func1("FIRE", "RPA")
'Hello World:FIRERPA'
>>> app.example_func2("FRI", "DA")
'Hello World:FRIDA'
>>> app.example_func3("FIRE", "RPA")
'FIRERPA'
```

## 调用导出方法（HTTP接口）

除了支持您使用客户端调用外，我们还支持您通过 HTTP 方式调用，您可以按照以下形式来使用。

```{tip}
自 8.15 版本开始，导出方法也支持使用 jsonrpc 2.0 协议调用（尚不支持 MultiCall 模式）。
```

您可以像如下方式，直接使用 jsonrpclib 进行调用，或者，如果您熟悉 JsonRPC 2.0 协议，您也可以自行编写代码进行请求，这是最规范并且可参考文档最全的一个协议实现。

```python
import jsonrpclib
server = jsonrpclib.Server('http://192.168.0.2:65000/script/com.android.settings/0')
server.example_func1("FIRE", "RPA")
```

当然，您仍然可以继续使用以下这种之前的调用协议，相对来说比较简单，但是不够规范。

```python
import json
import requests
url = "http://192.168.0.2:65000/script/com.android.settings/exampleFunc1?user=0"
res = requests.post(url, data={"args": json.dumps(["FIRE", "RPA"])})
print (res.status_code, res.json()["result"])
```

链接中的查询参数 `user` 为开应用的 UID，默认为 0 无需指定，如果是多开应用，需在此指定 UID。

以上代码通过 HTTP 的方式调用脚本导出接口。`com.android.settings` 为应用包名，`exampleFunc1` 为导出的函数名。请求的参数 `args` 必须使用 `json.dumps` 进行序列化。参数列表也可以同时使用多个参数，依据您的方法参数个数决定，提供空列表 `[]` 代表该导出函数无参数。

如果您的 FIRERPA 启用了接口证书，那么您需要使用 https 方式访问，并且需提供证书密码。

```python
headers = {"X-Token": "证书密码"}
res = requests.post(url, data={"args": json.dumps(["LAM", "DA"])}, headers=headers, verify=False)
print (res.status_code, res.json()["result"])
```

## HTTP 状态码

通过 HTTP 接口调用有特定的状态码，您可能需要根据他做判断，状态及其意义如下

| 状态码      | 状态码描述 |
| ----------- | ----------- |
| 200      | 一切正常       |
| 410   | 脚本未注入或未安装        |
| 500   | 脚本或参数异常        |
| 400   | 参数错误        |


## 问题排查

如果您通过 API 或者 HTTP 接口方式调用接口出现了卡死或者超时的问题，那大概率是因为您的应用处于后台被系统强制休眠，所以您可能需要使 APP 始终处于前台运行的状态。