使用 Proxyman Scripting 修改 WebSocket 消息的方法

使用 Proxyman Scripting 修改 WebSocket 消息的方法 的封面图

阅读约 6 分钟

Proxyman 现在可以实时修改 WebSocket 消息 🎉

只需几行 JavaScript,你就能编辑一条消息、丢弃它,或发送一条新消息。它同时支持 WS 和 WSS,并且两个方向都适用:从客户端到服务器,以及从服务器到客户端。

本文内容包括:

  • 测试 WebSocket 应用的难点。
  • 新增的 Modify WebSocket Messages 选项的作用。
  • 一个可自行运行的本地 WebSocket 服务器,附带逐步演示。
  • 现成的 WebSocket 模板,以及消息没有变化时的排查技巧。

Modify WebSocket Messages 是 Proxyman for macOS 的新功能。请从下载页面更新到最新版本,再按照本教程操作。

下面是最终效果。服务器发送的是真实的 BTC 价格,但应用收到的却是 $1。当应用发送密码时,Proxyman 会丢弃该消息,并向应用返回一条警告:

1. 问题所在

聊天、交易、游戏和实时仪表盘等应用都通过 WebSocket 获取实时数据。测试这类应用并不容易:

  • 你无法控制服务器。 它发送的是真实数据。要让它发送边界情况,例如价格为 0 或错误消息,就必须修改服务器代码。
  • 有些情况很难复现。 丢包、异常回复或连接断开,往往不会在你需要的时候出现。
  • 以前,Proxyman 只能读取 WebSocket 消息。 Scripting 中的 onWebsocket() 函数可以记录消息,但无法修改。

2. “Modify WebSocket Messages”是什么?

这是 Scripting 规则中的一个新复选框。开启后,Proxyman 会暂存每一条 WebSocket 消息,直到你的 onWebsocket() 函数返回。函数的返回值决定后续操作:

onWebsocket() 中的写法结果
return message发送该消息(包含你的修改)
return null丢弃该消息,对方不会收到
不返回任何值发送原始消息
abort()关闭两端的 WebSocket
sendToClient(data) / sendToServer(data)发送一条新消息。String 发送文本消息,Uint8Array 发送二进制消息

message 对象包含以下字段:

字段值
message.from"client" 或 "server"
message.type"text"、"binary"、"ping"、"pong" 或 "close"
message.data消息的文本内容。修改它即可编辑文本消息
message.rawData二进制消息的字节,类型为 Uint8Array

该选项默认关闭。关闭时,onWebsocket() 与之前一样,只读取消息,不会延迟它们。

3. 分步操作:修改实时 WebSocket

我们将搭建视频中的演示:一个发送 BTC 价格的小型服务器,以及一个通过 Proxyman 与其通信的客户端。

3.1 启动 WebSocket 服务器

创建一个新文件夹,并安装 ws 包:

mkdir websocket-demo && cd websocket-demo
npm init -y
npm install ws

将以下代码保存为 server.js。它每秒发送一次价格,并回显每一条聊天消息:

// server.js: a WebSocket server on ws://localhost:3456
const WebSocket = require('ws');
const server = new WebSocket.Server({ port: 3456 });

server.on('connection', (socket) => {
  // Send a BTC price every second
  const timer = setInterval(() => {
    const price = Number((67000 + Math.random() * 1000).toFixed(2));
    socket.send(JSON.stringify({ type: 'price', symbol: 'BTC', price: price }));
  }, 1000);

  // Print what the client sends, then echo it back
  socket.on('message', (data) => {
    console.log('SERVER gets:', data.toString());
    socket.send(JSON.stringify({ type: 'echo', text: JSON.parse(data).text }));
  });

  socket.on('close', () => clearInterval(timer));
});

console.log('WebSocket server on ws://localhost:3456');

在终端中运行:

node server.js

3.2 通过 Proxyman 连接客户端

Node.js 不会使用 macOS 的代理设置,因此客户端需要将 WebSocket 握手请求直接发送给 Proxyman。将以下代码保存为 client.js:

// client.js: connects to the server through Proxyman
const http = require('http');
const WebSocket = require('ws');

// Proxyman's port. You can see it in the Proxyman toolbar
const PROXYMAN_PORT = 9090;

// Sends the WebSocket handshake to Proxyman, like an app that uses an HTTP proxy
class ProxymanAgent extends http.Agent {
  addRequest(request, options) {
    request.path = 'http://' + options.host + ':' + options.port + request.path;
    super.addRequest(request, { ...options, host: '127.0.0.1', port: PROXYMAN_PORT });
  }
}

const socket = new WebSocket('ws://localhost:3456/prices', { agent: new ProxymanAgent() });

socket.on('open', () => {
  setTimeout(() => socket.send(JSON.stringify({ type: 'chat', text: 'Hello' })), 2000);
  setTimeout(() => socket.send(JSON.stringify({ type: 'chat', text: 'my password is hunter2' })), 4000);
});

socket.on('message', (data) => console.log('CLIENT gets:', data.toString()));

打开 Proxyman,然后在第二个终端窗口中运行客户端:

node client.js

你会在 Proxyman 中看到 ws://localhost:3456/prices。打开它的 Websocket 标签页即可查看消息。此时尚未做任何修改。按 Ctrl+C 停止客户端。

3.3 创建 Scripting 规则

  1. 在 Proxyman 中,右键点击 ws://localhost:3456/prices 会话,依次选择 Tools → Scripting...。Proxyman 会创建一条已填入该 URL 的规则。
  2. 保持 Request 勾选,并勾选 Modify WebSocket Messages。
  3. 将脚本替换为下面的代码,然后点击 Save & Activate(⌘S)。
Proxyman 脚本编辑器,已勾选 Modify WebSocket Messages,并包含 onWebsocket() 脚本
Proxyman 脚本编辑器,已勾选 Modify WebSocket Messages,并包含 onWebsocket() 脚本
function onWebsocket(context, url, request, response, message) {
  // Only change text messages. Binary, ping, pong, and close messages pass through
  if (message.type !== "text") {
    return message;
  }

  const json = JSON.parse(message.data);

  // 1. Server → client: every BTC price becomes $1
  if (message.from === "server" && json.type === "price") {
    json.price = 1;
    json.note = "edited by Proxyman";
    message.data = JSON.stringify(json);
    return message;
  }

  // 2. Client → server: drop a message with a password, and warn the client
  if (message.from === "client" && json.text.includes("password")) {
    sendToClient(JSON.stringify({ type: "warning", text: "Proxyman blocked a password" }));
    return null;
  }

  // 3. Everything else: send it as it is
  return message;
}

3.4 再次运行并查看结果

再次运行 node client.js。Proxyman 会在 WebSocket 连接时决定是否启用该选项,因此保存规则后,请务必重新连接。

现在,客户端收到的是带有备注 "edited by Proxyman" 的 "price": 1。包含密码的消息永远不会到达服务器:服务器终端只会打印 Hello,而客户端则会收到脚本发出的警告。

左图:客户端和服务器实际收发的内容。右图:Proxyman 中的 Websocket 标签页,显示已修改的价格
左图:客户端和服务器实际收发的内容。右图:Proxyman 中的 Websocket 标签页,显示已修改的价格

在 Proxyman 的 Websocket 标签页中,可以看到已修改的价格、脚本发送的警告,以及被丢弃的密码消息。

Proxyman 主窗口,显示 ws://localhost:3456/prices 会话及其已修改的 WebSocket 消息
Proxyman 主窗口,显示 ws://localhost:3456/prices 会话及其已修改的 WebSocket 消息

4. 更多示例

双向替换文本,例如切换主机名:

const FIND_REGEX = /staging\.example\.com/g;
const REPLACE_WITH = "api.example.com";

function onWebsocket(context, url, request, response, message) {
  if (message.type === "text") {
    message.data = message.data.replace(FIND_REGEX, REPLACE_WITH);
  }
  return message;
}

模拟连接断开:收到 10 条消息后断开连接,用于测试应用的重连逻辑。函数外部的代码会在每个连接中运行一次,因此每个连接都会单独计数:

const CLOSE_AFTER_MESSAGES = 10;
let messageCount = 0;

function onWebsocket(context, url, request, response, message) {
  messageCount += 1;
  if (messageCount === CLOSE_AFTER_MESSAGES) {
    console.log("Closing the WebSocket after " + messageCount + " messages");
    abort();
  }
  return message;
}

修改二进制消息:设置新的字节内容:

function onWebsocket(context, url, request, response, message) {
  if (message.type === "binary" && message.from === "server") {
    message.rawData = new Uint8Array([1, 2, 3]);
  }
  return message;
}

5. 使用 WebSocket 模板快速上手

无需从零编写脚本。在脚本编辑器中点击 Templates,然后搜索 “websocket”:

  • Log WebSocket Messages:在 Console Log 中打印每一条消息。它只读取消息,不做修改。
  • Modify JSON WebSocket Messages:修改服务器发送的 JSON 消息中的某个字段。
  • Replace Text in WebSocket Messages:双向查找并替换消息中的文本。
  • Drop WebSocket Messages:丢弃匹配正则表达式的消息,例如埋点事件。
  • Reply to a WebSocket Message:使用 sendToClient() 回复客户端的消息。服务器不会收到这条消息。
  • Simulate a WebSocket Disconnect:收到若干条消息后关闭 WebSocket。

点击 Replace 即可使用模板。Proxyman 还会自动勾选所需的复选框,包括 Modify WebSocket Messages。

Proxyman 中的 Script Templates 面板,已筛选出 WebSocket 模板
Proxyman 中的 Script Templates 面板,已筛选出 WebSocket 模板

6. 常见问题与解决方法

消息没有变化

  • 勾选 Modify WebSocket Messages,然后点击 Save & Activate(⌘S)。
  • 重新连接 WebSocket。该选项在 WebSocket 连接时读取,而不是对每条消息都读取。
  • 保持 Request 或 Response 至少一项被勾选。两项都未勾选的规则根本不会运行,包括 onWebsocket()。
  • 在脚本编辑器中打开 Console Log。如果脚本抛出错误,Proxyman 会发送原始消息,并在此处打印错误信息。

在 Proxyman 中看不到我的 WebSocket

  • 对于 wss://,请像处理 HTTPS 一样,为该域名启用 SSL Proxying。
  • 许多应用和库会跳过 localhost 的代理。请像 client.js 那样将流量发送到 Proxyman,或者按照你所用平台的 WebSocket 文档 进行设置。

注意事项

  • 只有匹配规则的 WebSocket 会等待脚本处理,其他流量照常传输。
  • 超过 16 MB 的消息会原样发送,不经过脚本处理。
  • 如果你使用 AI 智能体,Proxyman MCP 可以创建相同的规则:create_scripting_rule 接受 modify_websocket_messages: true 参数。

延伸阅读

Noah Tran
Noah Tran

无需改动服务器,即可修改你的 WebSocket 消息

在 Scripting 规则中开启 Modify WebSocket Messages,测试应用需要处理的每一条消息。

下载适用于 macOS 的 Proxyman