文章正文

CDP 远程调试方案

2022-04-19 发布于 · 阅读量:337

什么是 cdp 协议

cdp 协议简称 chrome 调试协议,是基于 scoket(websocket、usb、adb )消息的 json rpc 协议。用来调用 chrome 内部的方法实现 js,css ,dom 的开发调试。 可以将 实现了 cdp 协议的应用 看做 rpc 调用的服务端( chrome ,puppeteer), 将调试面板看做 rpc 调用的客户端(devtools)。

devtools 调试系统

完整的调试系统分别由前端,后端,协议,通道四部分组成

  • Frontend:调试器前端,如 chrome-devtools-frontend 一个 web 应用,接收 cdp 事件&发送 cdp 命令,实现 dom,network,debugger 的执行调试。
  • Backend: 实现了 cdp 协议的应用,会接收 cdp 命令&发送 cdp 事件。Chromium、V8 或 Node.js
  • Protocol:cdp 调试协议,调试器前端和后端使用此协议通信。 它分为代表被检查实体的语义方面的域。 每个域定义类型、命令(从前端发送到后端的消息)和事件(从后端发送到前端的消息)。该协议基于 json rpc 2.0 运行;
  • Channels:消息通道,后端和前端之间发送协议消息的一种方式。包括:Embedder Channel、WebSocket Channel、Chrome Extensions Channel、USB/ADB Channel。

chrome 远程调试

如上,所有能够消费发送 cdp 消息的应用都可以被 chrome-devtools-frontend 调试。chrome 集成了cdp协议,所以只需要通过消息通道与 chrome-devtools-frontend 应用 建立 socket 链接就可以被调试端调试。调试启动分为三步:

  • 启动 cdp 服务端
  • 启动 cdp 客户端
  • 创建链接

启动 cdp 服务端

以 Chrome,puppetter 为例启动 backend 应用,设置调试端口 9222。

  • chrome
    /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 --user-data-dir=./test
  • puppetter
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({
    args: ['--remote-debugging-port=9222', '--remote-debugging-address=0.0.0.0'],
  });
  const page = await browser.newPage();
  // let res= await page.goto('https://www.baidu.com');
  console.log(browser.wsEndpoint());
  // output -> ws://127.0.0.1:57724/devtools/browser/705082a5-19e6-4e9a-b8a6-477b7b6e1bd6
})();

此时访问 http://0.0.0.0:9222/json 此时可以获取调试页面 id 和 页面的 webSocketDebuggerUrl ,代表 backend 端启动成功。

[ {
   "description": "",
   "devtoolsFrontendUrl": "/devtools/inspector.html?ws=0.0.0.0:9222/devtools/page/659E9C174913FC5670B5F6E78B6B91FD",
   "id": "659E9C174913FC5670B5F6E78B6B91FD",
   "title": "欢迎使用 Chrome",
   "type": "page",
   "url": "chrome://welcome/",
   "webSocketDebuggerUrl": "ws://0.0.0.0:9222/devtools/page/659E9C174913FC5670B5F6E78B6B91FD"
}, {
   "description": "",
   "devtoolsFrontendUrl": "/devtools/inspector.html?ws=0.0.0.0:9222/devtools/page/9902C5D5F55923DFC457B7AA423F957E",
   "faviconUrl": "https://cn.bing.com/sa/simg/favicon-2x.ico",
   "id": "9902C5D5F55923DFC457B7AA423F957E",
   "title": "必应",
   "type": "page",
   "url": "https://cn.bing.com/",
   "webSocketDebuggerUrl": "ws://0.0.0.0:9222/devtools/page/9902C5D5F55923DFC457B7AA423F957E"
}, {
   "description": "",
   "devtoolsFrontendUrl": "/devtools/inspector.html?ws=0.0.0.0:9222/devtools/page/428C9AC3F9B6AF7235CC41D849DD7589",
   "faviconUrl": "https://www.baidu.com/favicon.ico",
   "id": "428C9AC3F9B6AF7235CC41D849DD7589",
   "title": "百度一下,你就知道",
   "type": "page",
   "url": "https://www.baidu.com/",
   "webSocketDebuggerUrl": "ws://0.0.0.0:9222/devtools/page/428C9AC3F9B6AF7235CC41D849DD7589"
} ]

启动 cdp 客户端

chrome-devtools-frontend 作为一个纯 web 应用,可以通过 koa 部署访问。同时 chrome 默认也集成了 chrome-devtools-frontend 服务。在 cdp 服务端启动后可以直接访问 http://0.0.0.0:9222/devtools/inspector.html

创建链接

传入 socket_backend_url ,http://0.0.0.0:9222/devtools/inspector.html?ws=${webSocketDebuggerUrl}

 或者直接访问 [http://0.0.0.0:9222/](http://0.0.0.0:9222/devtools/inspector.html?ws=0.0.0.0:9222/devtools/page/9902C5D5F55923DFC457B7AA423F957E) 可以看到浏览器中的的tab线程页,点击需要调试的页面,[http://0.0.0.0:9222/devtools/inspector.html?ws=0.0.0.0:9222/devtools/page/9902C5D5F55923DFC457B7AA423F957E](http://0.0.0.0:9222/devtools/inspector.html?ws=0.0.0.0:9222/devtools/page/9902C5D5F55923DFC457B7AA423F957E) , 就可以连接 [chrome-devtools-frontend](https://github.com/ChromeDevTools/devtools-frontend) 调试了。

此时打开 network,查看 ws ,可以看到 backend 端在接收 chrome-devtools-frontend 发送的 cdp 命令,并将 dom,network,资源信息返回给远程调试端

cdp协议结构

    cdp 协议按域「Domain」划分能力,每个域下有 Method、Event 和 Types。([https://chromedevtools.github.io/devtools-protocol/tot/Debugger/](https://chromedevtools.github.io/devtools-protocol/tot/Debugger/) )

Method 对应 socket 通信的请求/响应模式,Events 对应 socket 通信的发布/订阅模式,Types 为交互中使用到的实体。

用远程执行一段 js 为例,在 chrome-devtools-frontend 控制台输入下面代码,backend 端分别收到执行三个 cdp 命令。 Runtime.compileScript->Runtime.evaluate->Runtime.compileScript

pp=function(){
  alert(244)
}
pp()
{
    "id": 47,
    "method": "Runtime.evaluate",
    "params": {
        "expression": "pp=function(){alert(244)}",
        "includeCommandLineAPI": true,
        "generatePreview": true,
        "userGesture": false,
        "awaitPromise": false,
        "throwOnSideEffect": true,
        "timeout": 500,
        "disableBreaks": true,
        "replMode": true,
        "uniqueContextId": "-3083772769491583108.6833303176535544523"
    }
}
{
    "id": 178,
    "method": "Runtime.compileScript",
    "params": {
        "expression": "pp=function(){alert(244)}",
        "sourceURL": "",
        "persistScript": false,
        "executionContextId": 7
    }
}
{
    "id": 100,
    "method": "Runtime.compileScript",
    "params": {
        "expression": "pp()",
        "sourceURL": "",
        "persistScript": false,
        "executionContextId": 7
    }
}

手动实现

H5 端通过 CDP 协议调用调试

var ws = new WebSocket('ws://127.0.0.1:9222/devtools/page/620F91C22D41B614947001C52AC55E53');

window.ppp=function(){
// 调用 Command

  debugger
  ws.onmessage = function(event) {
    console.log(event.data);
    // 获取数据:{"method": "Page.loadEventFired", "params": {"timestamp": 1402317772.874949}}
  };
  ws.send('{"id": 1, "method": "Page.navigate", "params": {"url": "http://www.github.com"}}');
}

远程调试

chrome 社区提供了基础的远程调试方案 :devtools-remote 。 分别提供 websocket 服务做消息转发 和 chrome 插件在 backend 端来监听执行发送 cdp 消息。

devtools-remote 调试插件在 background 层实现 cdp 消息监听, 响应和执行。主要依赖下面几个 api 。 Chrome Extensions Api(chrome.debugger

  • chrome.debugger.getTargets: 获取可调试目标的列表( tab_id )
  • chrome.debugger.attach: 将调试器附加到指定目标。
  • chrome.debugger.sendCommand: 将 cdp 命令发送至调试目标。
  • chrome.debugger.onEvent.addListene: 监听指定选项卡页中的所有事件,在回调函数 response cdp 消息到 scocket 服务端。

插件内部逻辑实现

  1. 点击插件(chrome.browserAction.onClicked.addListener)获取当前页 tabID ,即目标 id.
  2. 调用 chrome.debugger.attach,将调试器附加到指定目标。
  3. socket connect 连接代理 websocket 服务器。(代理服务器连接调试应用 chrome-devtools-frontend
  4. 插件连接成功: 监听从代理服务器推送的 data.request 类型消息。 调用 chrome.debugger.sendCommand 执行 cdp 协议的命令, 将执行结果返回给socket服务端消息事件名为 data.response 消息 id 保持一致)。
  5. 调用 chrome.debugger.onEvent.addListener,监听调试页的所有事件,转成 cdp 的消息结构 {method: method, params: params} ,发送消息事件名为 data.event 到代理服务器,最终转发到调试应用

socket_proxy 逻辑 代理服务器的逻辑很简单,将来自插件端 'data.response' , 'data.event' 的事件消息转发到调试应用上。 将来自调试应用 'data.request' 事件消息转发到 backend 插件端。

此外社区还有 chobitsu 这样的 cdp 解析库,提供了 Storage、Runtime、Network、DOM、DOMDebugger、DOMStorage 的能力。与需要依赖在插件 background 层执行 chrome.debugger API 的方案不同。chobitsu 在浏览器运行时环境中手动实现了cdp 协议。 基于chobitsu 的能力可以实现待调试页面通过加载 backend.js 监听浏览器事件 执行 cdp 命令,实现 dom css network 的远程调试。 具体用例可以参考 devtools-pro 。由此方案进一步联想到xss安全的重要性。

同理 react-devtools 的实现方案,也与cdp方案 类似, 在调试页面中引入 或者 通过插件插入 backend.js,监听变化发送到调试应用。

参考文档

chrome DevTools Protocol

chrome-remote-interface

深入理解 Chrome DevTools

❉ 作者介绍 ❉