上一主题 下一主题
ScriptCat,新一代的脚本管理器脚本站,与全世界分享你的用户脚本油猴脚本开发指南教程目录
返回列表 发新帖
楼主: 李恒道 - 

自写Webscoket实现CDP协议联通

[复制链接]
  • TA的每日心情
    慵懒
    2024-10-28 07:07
  • 签到天数: 193 天

    [LV.7]常住居民III

    712

    主题

    5966

    回帖

    6763

    积分

    管理员

    非物质文化遗产社会摇传承人

    积分
    6763

    荣誉开发者喜迎中秋油中2周年生态建设者

    发表于 2023-1-14 21:01:41 | 显示全部楼层 | 阅读模式

    前文

    跟一之哥有了那个想法
    被毒打之后
    个人还是有一点不服
    于是决定开始尝试CDP协议的分析和学习

    原理介绍

    Chrome的调试和开发者工具实际是分离的
    chrome内部起一个webscoket服务器
    如果是使用Chrome内置的开发者工具
    并不会使用Webscoket
    而是使用chrome内部的全局函数进行通信
    但是其大致原理是不变的
    我们可以将chrome内部的服务器称之为backed
    而将vscode或者f12等进行调试的称之为fronted
    通信的协议称之为CDP协议
    而诸如Websocket,或全局函数称之为通信的媒介
    以上来自于神光大佬的前端调试通关秘籍
    网址https://s.juejin.cn/ds/kuLGA1b/
    大家有兴趣也可以买一本

    实践

    那我们可以开始尝试一下,首先创建一个快捷方式,在末尾加上
    --remote-debugging-port=9222
    然后启动
    注意这里,启动之前必须打开任务管理器确保没有chrome
    有时候可能挂在后台进程,导致启动合并而没有打开端口
    接下来我们尝试一下链接
    这里我用的ws库

    var WebSocketClient = require("ws");
    const chromePort = 9222;
    var chromeSocket = new WebSocketClient("ws://localhost:" + chromePort);

    发现失败了
    为什么?
    我们可以看一个已经完成的实现【chrome-remote-interface】
    他的使用方式还是挺简单的,我们看一下demo

    async function example() {
      let client;
      try {
        // connect to endpoint
        client = await CDP();
        // extract domains
        const { Network, Page } = client;
        // setup handlers
        // enable events then start!
        await Network.enable();
        await Page.enable();
        await Page.navigate({ url: "https://github.com" });
        await Page.loadEventFired();
      } catch (err) {
        console.error(err);
      } finally {
        if (client) {
          await client.close();
        }
      }
    }
    
    example();

    那我们就去看看CDP源码

    function CDP(options, callback) {
        if (typeof options === 'function') {
            callback = options;
            options = undefined;
        }
        const notifier = new EventEmitter();
        if (typeof callback === 'function') {
            // allow to register the error callback later
            process.nextTick(() => {
                new Chrome(options, notifier);
            });
            return notifier.once('connect', callback);
        } else {
            return new Promise((fulfill, reject) => {
                notifier.once('connect', fulfill);
                notifier.once('error', reject);
                new Chrome(options, notifier);
            });
        }
    }

    可以发现默认应该是调用了new Chrome
    我们继续往里看

    class Chrome extends EventEmitter {
        constructor(options, notifier) {
            super();
            // options
            const defaultTarget = (targets) => {
                // prefer type = 'page' inspectable targets as they represents
                // browser tabs (fall back to the first inspectable target
                // otherwise)
                let backup;
                let target = targets.find((target) => {
                    if (target.webSocketDebuggerUrl) {
                        backup = backup || target;
                        return target.type === 'page';
                    } else {
                        return false;
                    }
                });
                target = target || backup;
                if (target) {
                    return target;
                } else {
                    throw new Error('No inspectable targets');
                }
            };
            options = options || {};
            this.host = options.host || defaults.HOST;
            this.port = options.port || defaults.PORT;
            this.secure = !!(options.secure);
            this.useHostName = !!(options.useHostName);
            this.alterPath = options.alterPath || ((path) => path);
            this.protocol = options.protocol;
            this.local = !!(options.local);
            this.target = options.target || defaultTarget;
            // locals
            this._notifier = notifier;
            this._callbacks = {};
            this._nextCommandId = 1;
            // properties
            this.webSocketUrl = undefined;
            // operations
            this._start();
        }
    }

    可以没什么操作,调用了_start()
    我们继续往里看

        async _start() {
            const options = {
                host: this.host,
                port: this.port,
                secure: this.secure,
                useHostName: this.useHostName,
                alterPath: this.alterPath
            };
            try {
                // fetch the WebSocket debugger URL
                const url = await this._fetchDebuggerURL(options);
                // allow the user to alter the URL
                const urlObject = parseUrl(url);
                urlObject.pathname = options.alterPath(urlObject.pathname);
                this.webSocketUrl = formatUrl(urlObject);
                // update the connection parameters using the debugging URL
                options.host = urlObject.hostname;
                options.port = urlObject.port || options.port;
                // fetch the protocol and prepare the API
                const protocol = await this._fetchProtocol(options);
                api.prepare(this, protocol);
                // finally connect to the WebSocket
                await this._connectToWebSocket();
                // since the handler is executed synchronously, the emit() must be
                // performed in the next tick so that uncaught errors in the client code
                // are not intercepted by the Promise mechanism and therefore reported
                // via the 'error' event
                process.nextTick(() => {
                    this._notifier.emit('connect', this);
                });
            } catch (err) {
                this._notifier.emit('error', err);
            }
        }

    发现这里进行了Websocket的链接
    await this._connectToWebSocket();
    追一下发现他直接进行了链接,传入的是webSocketUrl
    图片.png
    我们可以发现他用的也是ws库,那基本可能就是webSocketUrl的问题了
    图片.png
    我们看一下他的webSocketUrl哪里来的
    搜一下找到了_start里

                // fetch the WebSocket debugger URL
                const url = await this._fetchDebuggerURL(options);
                // allow the user to alter the URL
                const urlObject = parseUrl(url);
                urlObject.pathname = options.alterPath(urlObject.pathname);
                this.webSocketUrl = formatUrl(urlObject);

    看一下fetchDebuggerURL里

        // fetch the WebSocket URL according to 'target'
        async _fetchDebuggerURL(options) {
            const userTarget = this.target;
            switch (typeof userTarget) {
            case 'string': {
                let idOrUrl = userTarget;
                // use default host and port if omitted (and a relative URL is specified)
                if (idOrUrl.startsWith('/')) {
                    idOrUrl = `ws://${this.host}:${this.port}${idOrUrl}`;
                }
                // a WebSocket URL is specified by the user (e.g., node-inspector)
                if (idOrUrl.match(/^wss?:/i)) {
                    return idOrUrl; // done!
                }
                // a target id is specified by the user
                else {
                    const targets = await devtools.List(options);
                    const object = targets.find((target) => target.id === idOrUrl);
                    return object.webSocketDebuggerUrl;
                }
            }
            case 'object': {
                const object = userTarget;
                return object.webSocketDebuggerUrl;
            }
            case 'function': {
                const func = userTarget;
                const targets = await devtools.List(options);
                const result = func(targets);
                const object = typeof result === 'number' ? targets[result] : result;
                return object.webSocketDebuggerUrl;
            }
            default:
                throw new Error(`Invalid target argument "${this.target}"`);
            }
        }

    根据userTarget判断不同类型,他来自于this.target;
    如果不给任何参数,默认来自于

            const defaultTarget = (targets) => {
                // prefer type = 'page' inspectable targets as they represents
                // browser tabs (fall back to the first inspectable target
                // otherwise)
                let backup;
                let target = targets.find((target) => {
                    if (target.webSocketDebuggerUrl) {
                        backup = backup || target;
                        return target.type === 'page';
                    } else {
                        return false;
                    }
                });
                target = target || backup;
                if (target) {
                    return target;
                } else {
                    throw new Error('No inspectable targets');
                }
            };

    相当于获取所有targets,看代码来说基本是获取第一个存在webSocketDebuggerUrl,并且type为page的,那targets哪里来的?
    我们找到devtools.List(options)里,发现了

    function List(options, callback) {
        devToolsInterface('/json/list', options, (err, tabs) => {
            if (err) {
                callback(err);
            } else {
                callback(null, JSON.parse(tabs));
            }
        });
    }

    devToolsInterface类似于直接访问网页
    我们直接访问http://127.0.0.1:9222/json/list
    图片.png
    就找到了socket的地址,那我们自己实现一下

    var WebSocketClient = require("ws").WebSocket;
    var axios = require("axios");
    const chromePort = 9222;
    function getWebscoketUrl() {
      return new Promise((resolve) => {
        axios
          .get("http://127.0.0.1:" + chromePort + "/json/list")
          .then((response) => {
            const target = response.data.find((item) => {
              return (
                item.webSocketDebuggerUrl !== undefined && item.type === "page"
              );
            });
            resolve(target.webSocketDebuggerUrl);
          });
      });
    }
    async function main() {
      const getWebsocketUrl= await getWebscoketUrl();
      var chromeSocket = new WebSocketClient(getWebsocketUrl);
      chromeSocket.onopen = function (e) {
      console.log("Connection to server opened");
        };
    }
    main();

    测试发现正常提示了Connection to server opened
    那我们就完成了对CDP的连结

    结语

    撒花~

    混的人。
    ------------------------------------------
    進撃!永遠の帝国の破壊虎---李恒道

    入驻了爱发电https://afdian.net/a/lihengdao666
    个人宣言:この世界で私に胜てる人とコードはまだ生まれていません。死ぬのが怖くなければ来てください。
  • TA的每日心情
    开心
    2024-7-30 00:00
  • 签到天数: 122 天

    [LV.7]常住居民III

    29

    主题

    601

    回帖

    542

    积分

    专家

    积分
    542

    油中2周年生态建设者油中3周年挑战者 lv2

    发表于 2023-1-15 19:18:27 | 显示全部楼层
    我就知道,我就知道没那么简单
    道哥哥一开动脑筋必定飞沙走石鬼见愁,惊雷这通天修为天塌地陷紫金锤

    哥哥知道吗,卓别林唯一一点比不上希特勒的地方是卓别林没有手下
    回复

    使用道具 举报

  • TA的每日心情
    慵懒
    2024-10-28 07:07
  • 签到天数: 193 天

    [LV.7]常住居民III

    712

    主题

    5966

    回帖

    6763

    积分

    管理员

    非物质文化遗产社会摇传承人

    积分
    6763

    荣誉开发者喜迎中秋油中2周年生态建设者

    发表于 2023-1-15 22:00:09 | 显示全部楼层
    脚本体验师001 发表于 2023-1-15 19:18
    我就知道,我就知道没那么简单
    道哥哥一开动脑筋必定飞沙走石鬼见愁,惊雷这通天修为天塌地陷紫金锤

    笑死了
    哥哥都喊上社会语录了
    混的人。
    ------------------------------------------
    進撃!永遠の帝国の破壊虎---李恒道

    入驻了爱发电https://afdian.net/a/lihengdao666
    个人宣言:この世界で私に胜てる人とコードはまだ生まれていません。死ぬのが怖くなければ来てください。
    回复

    使用道具 举报

  • TA的每日心情
    开心
    前天 13:37
  • 签到天数: 213 天

    [LV.7]常住居民III

    305

    主题

    4196

    回帖

    4061

    积分

    管理员

    积分
    4061

    管理员荣誉开发者油中2周年生态建设者喜迎中秋油中3周年挑战者 lv2

    发表于 2023-1-17 09:59:22 | 显示全部楼层

    什么是 cdp 协议

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

    哥哥用websocket实现了也可以,学习了!

    上不慕古,下不肖俗。为疏为懒,不敢为狂。为拙为愚,不敢为恶。
    回复

    使用道具 举报

    发表回复

    本版积分规则

    快速回复 返回顶部 返回列表