# Scripting（脚本）

> 使用 Proxyman Scripting 工具，通过 JavaScript 修改 HTTP 请求和响应、模拟 API，并使用 onWebsocket 记录 WebSocket 消息。

## 1. 这是什么？ {#1-whats-it}

Proxyman 提供了 Scripting 工具。你可以编写 JavaScript 代码，以灵活的方式修改请求和响应。

## 2. 优势 {#2-benefits}

* 使用 JS 代码实现 Map Local / Map Remote / Breakpoint，**速度快 100 倍**
* 修改**请求内容**，包括 Domain、Host、Scheme、Port、Path、HTTP Method、HTTP Headers、Query、Body（Encoded-Form、JSON、纯文本）
* 使用 `onWebsocket` 记录和检查 WebSocket 消息（**macOS、Windows 和 Linux 版 Proxyman 26.1.0 或更高版本**）。
* 使用 `onWebsocket` [编辑、丢弃或发送 WebSocket 消息](https://proxyman.com/docs/zh-cn/scripting/modify-websocket-messages)（**macOS 版 Proxyman 26.2.0 或更高版本，Windows/Linux 版 Proxyman 26.1.0 或更高版本**）。
* 修改**响应内容**，包括 HTTP Status Code、HTTP Headers 和 Body（JSON、Encoded-Form、纯文本、二进制等）
* 内置大量常用插件和库，例如 Hashing、Encode/Decode、JSON-Text transformer、Beautify 等
* **编写你自己的 JS 插件或库**
* 旨在替代 Charles Proxy 的 Rewrite 图形界面工具
* 通过 [Shared State 或环境变量](https://proxyman.com/docs/zh-cn/scripting/environment-variables)，在当前会话的脚本之间共享状态

![Proxyman 脚本编辑器，显示一条 POST 规则，已勾选 Request 和 Response，启用了 Run as Mock API，并包含 onRequest 和 onResponse 代码](https://proxyman.com/docs/assets/proxyman-script-editor.jpg)

_Scripting 工具_

- [代码片段](https://proxyman.com/docs/zh-cn/scripting/snippet-code)

## 3. 如何使用？ {#3-how-to-use-it}

你可以通过以下方式打开 Scripting 工具：

* **Scripting** 菜单 -> **Script List…**（⌥⌘I）
* 右键点击一个请求 -> **Tools** -> **Scripting…**

查看 [代码片段](https://proxyman.com/docs/zh-cn/scripting/snippet-code) 页面，了解 Scripting 工具的 JS 代码片段合集。

## 4. 在 GraphQL 请求中使用 Scripting {#4-scripting-with-graphql-requests}

从 Proxyman 2.27.0 版本开始，Scripting 工具可以通过 QueryName 匹配 GraphQL 请求。请参阅以下 GraphQL 文档。

- [GraphQL 调试](https://proxyman.com/docs/zh-cn/advanced-features/graphql)

## 4.1 在 WebSocket 中使用 Scripting {#41-scripting-with-websocket}

从 macOS 版 Proxyman 6.2.0 开始，可以使用 `onRequest` 和 `onResponse` 修改 WebSocket 请求的 URL 和握手请求头。请参阅[修改 WebSocket URL 和请求头](https://proxyman.com/docs/zh-cn/scripting/snippet-code#change-websocket-url-requestsresponse-header)。

### 使用 onWebsocket 记录消息 {#log-messages-with-onwebsocket}

> **注意:** `onWebsocket` 可在 **macOS、Windows 和 Linux 版 Proxyman 26.1.0 或更高版本**中使用，iOS 版不支持。

使用 `onWebsocket(context, url, request, response, message)` 检查并记录从客户端和服务器抓取的消息：

```javascript
function onWebsocket(context, url, request, response, message) {
  console.log(`[${message.from}] ${message.type}: ${message.data}`);
}
```

默认情况下，该钩子为**只读**。Proxyman 会先转发消息，再运行脚本。修改其参数或返回值都不会改变流量或已抓取的消息。无需 return 语句。

要编辑、丢弃或发送消息，请在规则上勾选 **Modify WebSocket Messages**（macOS 版 Proxyman 26.2.0 或更高版本，Windows/Linux 版 Proxyman 26.1.0 或更高版本）。请参阅 [Modify WebSocket Messages](https://proxyman.com/docs/zh-cn/scripting/modify-websocket-messages)。

**使用方法**

1. 抓取一个 WebSocket 连接。对于 `wss://`，请为该主机开启 SSL Proxying，以便 Proxyman 读取其中的消息。
2. 右键点击已抓取的请求，选择 **Tools → Scripting**。生成的脚本会在 `onResponse` 下方包含 `onWebsocket`。
3. 添加日志代码，然后点击 **Save & Activate**。
4. 重新连接 WebSocket，发送消息，然后查看脚本的 **Console Log**。

连接建立时，规则必须处于启用状态。创建规则后，或在连接建立时脚本尚未包含 `onWebsocket`、之后才添加该钩子，都需要重新连接。普通的新脚本模板不包含此钩子，你可以手动添加。

**参数**

必须保留全部五个参数并保持顺序。`context` 和 `url` 描述请求及匹配的规则。`request` 和 `response` 包含最终的握手信息，其中包括 `onRequest`、`onResponse` 或 Breakpoint 所做的修改。`message` 包含以下内容：

| 字段 | 值 |
| --- | --- |
| `message.type` | `"text"`、`"binary"`、`"ping"`、`"pong"` 或 `"close"`。 |
| `message.from` | `"client"` 或 `"server"`。 |
| `message.data` | 始终为字符串。二进制数据会尽可能以 UTF-8 文本显示，否则显示为 `Binary (length=N)`。不会应用可选的 WebSocket 标签页解码器。 |
| `message.rawData` | 复制得到的 `Uint8Array`，仅适用于二进制消息。需要显式记录；枚举 `message` 时不会包含该字段。 |
| `message.closeCode` | 数字，仅适用于关闭消息，例如 `1000`。 |

**注意事项**

* 分片的文本帧和二进制帧会在钩子运行前重新组装。你收到的是一条完整消息，而不是每个延续帧各触发一次回调。
* 不会提供 Ping/Pong 的负载数据和关闭原因。
* 支持同步和 `async` 钩子，并可使用 `console.log`、`require`、`sharedState` 和 `$http` 等辅助功能。
* 消息到达时，对现有钩子的编辑以及规则、文件夹和工具的启用状态，最多每秒检查一次。已排队的调用仍可能执行完成。
* Console Log 最多保留最新的 200 条日志。如果脚本处理不及时，流量会照常传输，但钩子调用可能会被跳过并显示警告。Console Log 用于调试，不是完整的流量存档。

文本、JSON、二进制、Ping/Pong 和关闭消息的示例，请参阅 [WebSocket 消息代码片段](https://proxyman.com/docs/zh-cn/scripting/snippet-code#log-websocket-messages-with-onwebsocket)。

## 5. 示例 {#5-examples}

以下指南介绍如何编写 JS 代码，将请求域名从生产环境改为 localhost，并修改响应体。

1. 创建脚本前，请先为该域名开启 SSL Proxying。
2. 打开 Scripting 工具，新建一个脚本条目（⌘N）。你也可以右键点击请求 -> Tools -> Scripting…，Proxyman 会自动为你创建脚本。
3. 为脚本命名，并定义 Matching Rule。
4. 例如：**Name**=在 localhost 上测试接口，**URL**=https://proxyman.io
5. 在 **Run Script on** 中勾选 **Request** 和 **Response** 复选框
6. 开始编写 `onRequest` 函数的 JS 代码

**onRequest(context, url, request) {}**

```javascript
// Import UUID addons
const { uuidv4 } = require("@addons/UUID.js");

function onRequest(context, url, request) {
  // print log
  console.log(request);

  // Change Production domain -> Localhost
  request.method = "GET";
  request.scheme = "http";
  request.host = "localhost";
  request.port = 8000;
  
  // Add new header
  request.headers["X-New-Header"] = "Hello From Scripting feature";
  request.headers["UUID"] = uuidv4(); // generate random UUIDv4
  delete request.headers["Key-Need-Delete"];

  // Update or Add a new Query
  request.queries["name"] = "Proxyman";

  // Update Body
  var body = request.body;
  body["name"] = "Proxyman";
  request.body = body;
  
  // Or Map the body with a local file (Proxyman 2.25.0+)
  // request.bodyFilePath = "~/Desktop/mockdata.json";
  
  // Done
  return request;
}
```

## onRequest() 对象格式 {#onrequest-object-format}

`context`、`url` 和 `request` 对象的定义如下：

```javascript
// context (readonly)
{
    "scriptName": "<String> Your Script Name",
    "matchingRule": "<String> Your Matching Rule",
    "matchingMethod": "<String> Method",
    "isEnableOnRequest": "Bool",
    "isEnableOnResponse": "Bool",
    "filePath": "<String> Script path",
    "flow": { // Available from 2.16.0+
        "serverPort": "443",
        "serverIpAddress": "104.18.230.83",
        "clientIpAddress": "192.168.0.102",
        "remoteDeviceName": "iPhone XR",
        "remoteDeviceIP": "192.168.0.102",
        "id": "51",
        "clientPath": null,
        "clientPort": "51494",
        "clientName": null,
        "mapRemoteOriginalURL": "<String> Original URL before Map Remote modification (nullable)"
    },
}

// url (readonly)
url: String // => Present the full URL

// request
{
    "method": "<String> HTTP Method. Accept string method. Ex: GET, POST, ...",
    "scheme": "<String> Accept http or https",
    "host": "<String> Host of the request. Ex: api.proxyman.io, localhost, ...",
    "path": "<String>: Path of the URL. Ex: /v1/data",
    "port": "<Int> Accept int port number. Ex: 443, 8080, ..",
    "queries": "<[String: Any]> A JS Object (Dictionary) contains key values of the query",
    "headers": "<[String: Any]> A JS Object (Dictionary) contains key values of the header",
    "body": "Depend on the Content-Type header. It might be a dictionary for JSON and form, Plain Text or Uint8Array",
    "bodyFilePath": "<String><Optional> Set a body with a local file. See example in Snippet Code Page"
    "rawBody": "<Readonly>: A raw body String or Uint8Array",
    "preserveHostHeader": "<Bool> Preserve the Host",
    "isURLEncoding": "<Bool> Determine if Proxyman will perform URLEncoding when constructing the final URL. Default is True",
    "upstream": "<Object><Optional> Upstream TLS ALPN and HTTP/2 settings. Proxyman v26.0.0 or later for macOS."
}

```

> **备注:** 除 `rawBody` 外，你可以修改 request 对象的任何值。

在 `onRequest` 中设置 `request.upstream`。示例和支持的字段，请参阅[上游 ALPN 和 HTTP/2 设置](https://proxyman.com/docs/zh-cn/scripting/snippet-code#upstream-alpn-and-http2-settings)。

> **备注:** 如果由于 Content-Type 请求头不正确，**body** 的格式无效，请使用 **rawBody** 并手动解析字符串。

`request.body` 的类型取决于 Content-Type 请求头。

| Content-Type 请求头 | request.body |
| ----------------------------------------------------------------------------------- | ----------------- |
| application/json 或 JSON 系列 | JavaScript 对象 |
| application/x-www-form-urlencoded | JavaScript 对象 |
| 纯文本或基于文本的 Content-Type，例如：application/js、text/css、text/html 等 | String |
| 其余类型：二进制数据（application/zip、application/octet-stream） | Uint8Array |

以下是代码片段页面中的常用 JS 代码：

- [代码片段](https://proxyman.com/docs/zh-cn/scripting/snippet-code)

7\. 开始编写 `onResponse` 函数的代码

```javascript
function onResponse(context, url, request, response) {
  console.log(response);

  // Update or Add a new header
  response.headers["Content-Type"] = "application/json";

  // Update status Code
  response.statusCode = 500;

  // Update Body
  var body = response.body;
  body["new-key"] = "Proxyman";
  response.body = body;

  // Or Map the body with a local file (Proxyman 2.25.0+)
  // response.bodyFilePath = "~/Desktop/mockdata.json";
  
  // Done
  return response;
}
```

## onResponse() 对象格式 {#onresponse-objects-format}

`context`、`url`、`request` 和 `response` 对象的定义如下：

```javascript
// context (readonly): Same with onRequest

// url (readonly): Same with onRequest

// request (readonly): Same with onRequest

// response
{
    "statusCode": "<Int> Status Code. Ex: 200, 400, 404,...",
    "httpVersion": "<String><Readonly> The HTTP Version",
    "statusPhrase": "<String><Readonly> HTTP Status Phrase. Ex Not Found, OK, ...",
    "headers": "<[String: Any]> A JS Object (Dictionary) contains key values of the header",
    "body": "Depend on the Content-Type header. It might be a dictionary for JSON and form, PlainText or Uint8Array",
    "rawBody": "<Readonly>: A raw body String or Uint8Array",
    "bodyFilePath": "<String><Optional> Set a body with a local file. See example in Snippet Code Page"
}
```

> **备注:** 你可以修改 response 对象中的 `statusCode`、`headers` 和 `body`。

> **备注:** 如果由于 Content-Type 请求头不正确，**body** 的格式无效，请使用 **rawBody** 并手动解析字符串。

`response.body` 的类型取决于 Content-Type 请求头。

| Content-Type 请求头 | response.body |
| ----------------------------------------------------------------------------------- | ----------------- |
| application/json 或 JSON 系列 | JavaScript 对象 |
| application/x-www-form-urlencoded | JavaScript 对象 |
| 纯文本或基于文本的 Content-Type，例如：application/js、text/css、text/html 等 | String |
| 其余类型（application/zip、application/octet-stream） | Uint8Array |

> **备注:** 你必须在 `onRequest` 和 `onResponse` 函数中返回 `request` 和 `response` 对象。

## 6. 内置插件和库 {#6-built-in-addons-and-libraries}

Proxyman 提供大量常用任务所需的插件和库，例如 Hashing、Encode/Decode 等。

- [内置插件（Addons）](https://proxyman.com/docs/zh-cn/scripting/addons)

- [内置 JS 库](https://proxyman.com/docs/zh-cn/scripting/built-in-js-libraries)

- [代码片段](https://proxyman.com/docs/zh-cn/scripting/snippet-code)

## 7. 调试 JS 错误 {#7-debugging-js-error}

有时你可能会因为语法错误、无效代码等原因遇到 JavaScript 错误。你可以通过阅读控制台中的错误信息，或使用 `console.log()` 进行调试。

## 8. 将 Scripting 用作 Mock API {#8-use-scripting-as-a-mock-api}

从 Proxyman 2.32.0 开始，你可以将 Scripting 工具用作 Mock API。请求不会发送到服务器，响应体由你自己定义。其工作方式与 Map Local 相同。

当真实的 REST API 尚未就绪时，这一功能很有用。你可以在本地定义并测试它。

启用 Mock API 的步骤如下：

1. 打开 Scripting 工具 -> 选择脚本
2. 在脚本编辑器中，于 **Request** 和 **Response** 复选框旁边勾选 **Run as Mock API**（参见上方的脚本编辑器截图）。

然后，你可以像平常一样定义响应：

```javascript
function onResponse(context, url, request, response) {

  // Init new body
  var body = {};
  body["new-key"] = "Proxyman";
  response.body = body;

  // Or map from a file
  // response.bodyFilePath = "~/Desktop/myfile.json"
  
  // Done
  return response;
}
```

## 9. 注意事项 {#9-notes}

* 你必须在 `onRequest` 和 `onResponse` 函数中返回 `request` 和 `response` 对象。
* **Proxyman 4.16.0 或更高版本**：Proxyman 现在会将二进制数据转换为 **Uint8Array**
* Proxyman 4.15.0 或更早版本：JavaScript 没有 Data 类型，因此二进制 body 会在 JavaScript 中被转换为 **Base64 编码字符串**。如需将 Uint8Array、blob 或 ArrayBuffer 传入 body，请先将其转换为 **Base64 编码字符串**。

## 了解更多 {#learn-more}

* [如何为 GraphQL 请求映射本地响应？](https://proxyman.com/posts/2021-01-24-How-To-Map_Response_For_GraphQL_Requests)
* [如何绕过 CORS 策略](https://proxyman.com/posts/2020-08-31-How-to-by-pass-CORS-policy)
