const { describe, test, expect, } = require('@jest/globals') const { WebDriver } = require('../common/web_driver') const { inflate, deflate, } = require('pako') const fs = require('fs') const path = require('node:path') // Static code + Reflection + .proto parser const protobuf = require('protobufjs') const desc = require('./hotconi.proto') const timeout = 9999999 async function sleep (ms) { return new Promise(resolve => setTimeout(resolve, ms)) } describe('hotcoin站点测试', () => { let targetInfo = { 'targetId': '', 'type': '', 'title': '', 'url': '', 'attached': true, 'canAccessOpener': false, 'browserContextId': '', } const driver = new WebDriver({ // port: 23035, }) const init = async () => { // await driver.setProxy('socks://localhost:8880') await driver.setProxy('http://172.16.1.132:3128') await driver.builder() let target = await driver.cdp.Target targetInfo = await target.getTargetInfo() console.info('info>>>>', targetInfo) } const baseUrl = 'https://www.hotcoin.com/' const prefix = 'hotcoin' let cookiesPath = path.join(__dirname, 'tmp', prefix + '_cookies') let storagePath = path.join(__dirname, 'tmp', prefix + '_storage') test('测试打开', async (done) => { await init() const page = driver.cdp.Page page.navigate({ url: baseUrl }) done() }, timeout) const login = async (Storage, Runtime) => { let cookie = '', localStorage = '' if (fs.existsSync(cookiesPath)) { cookie = fs.readFileSync(cookiesPath, 'utf8') } // 借助localStorage中的jwt信息登录 if (fs.existsSync(storagePath)) { localStorage = fs.readFileSync(storagePath, 'utf8') } if (cookie) { await Storage.setCookies(JSON.parse(cookie)) } if (localStorage) { // let script = ` let storage = JSON.parse(`${localStorage}`) || {} // for (const k in storage) { // localStorage.setItem(k,storage[k]); // } // ` // script=` // localStorage.setItem('aaa','bbb') // ` // await Runtime.evaluate({ // expression: `localStorage.setItem('aaa','>>>>>')`, // // awaitPromise: true, // }); for (const k in storage) { await Runtime.evaluate({ expression: `localStorage.setItem(\`${k}\`, \`${storage[k]}\`)`, // awaitPromise: true, }) } } } test('自动登录', async (done) => { await init() const { Storage, Page, Runtime, } = driver.cdp await Runtime.enable() try { Page.navigate({ url: baseUrl }) await login(Storage, Runtime) let t = Date.now() let s = setInterval(async () => { let cookies = await Storage.getCookies() fs.writeFileSync(cookiesPath, JSON.stringify(cookies), 'utf8') let localStorage = await Runtime.evaluate({ expression: 'JSON.stringify(localStorage)', }) fs.writeFileSync(storagePath, localStorage?.result?.value, 'utf8') if (Date.now() - t > 5 * 60 * 1000) { clearInterval(s) } }, 1000) } catch (e) { console.error(e) } await sleep(timeout) done() }, timeout) test('监听wss', async (done) => { await init() const { Storage, Page, Network, Runtime, } = driver.cdp Network.enable() Page.navigate({ url: baseUrl }) await login(Storage, Runtime) const decode = (data) => { let res = inflate(data/* , { to: 'string' } */) console.info('解压缩结果:', res) switch (res) { case 'pong': return res default: { // function decodeProtobufToJson(buffer) { // try { // // 创建一个通用的解码器 // const Reader = protobuf.Reader; // const reader = Reader.create(buffer); // // const result = {}; // // while (reader.pos < reader.len) { // const tag = reader.uint32(); // const fieldId = tag >>> 3; // const wireType = tag & 7; // // // 根据 wireType 解析数据 // let value; // switch (wireType) { // case 0: // varint // value = reader.uint64(); // break; // case 1: // 64-bit // value = reader.fixed64(); // break; // case 2: // length-delimited (string, bytes, embedded messages) // value = reader.bytes(); // // 尝试将 bytes 转换为字符串 // try { // value = new TextDecoder().decode(value); // } catch(e) {} // break; // case 5: // 32-bit // value = reader.fixed32(); // break; // default: // reader.skipType(wireType); // continue; // } // // result[`field${fieldId}`] = value; // } // // return result; // // } catch (error) { // console.error("解析错误:", error); // return null; // } // } // // let object = decodeProtobufToJson(res) // let root = protobuf.Root.fromJSON(desc.common) // // // 查找 Response 消息类型 // let ResponseType = root.lookupType('Response') // // // 解码数据 // let message = ResponseType.decode(res) // // // 转换为普通的 JavaScript 对象 // let object = ResponseType.toObject(message, { // longs: String, // 将 long 转换为字符串 // enums: String, // 将枚举转换为字符串 // bytes: String, // 将 bytes 转换为字符串 // defaults: true, // 包含默认值 // objects: true, // arrays: true, // json: true, // oneofs: true, // }) // console.info('=00==>', object) // let key = (object.ch || '') + '.json' // let key = (object.ch || '') + '.js' //root = protobuf.Root.fromJSON(require('./hotconi/proto/' + key)) // root = protobuf.loadSync(path.resolve('./hotconi/proto/' + key)) // root = require('./hotconi/proto/' + key) let root = require('./hotconi/proto/market.v2.trade.area.tickers') // Response = root.lookupType(object.ch + '.Response') // message = Response.decode(res) // // object = Response.toObject(message, { // longs: String, // enums: String, // bytes: String, // defaults: true, // arrays: true, // objects: true, // oneofs: true, // }) console.info('=11==>', 'object') // return JSON.parse(res) } } } // 一个二进制数据流还原 // opcode number WebSocket message opcode. // mask boolean WebSocket message mask. // payloadData string WebSocket message payload data. If the opcode is 1, this is a text message and payloadData is a UTF-8 string. // If the opcode isn't 1, then payloadData is a base64 encoded string representing binary data. const formatResponse = (params = { opcode: 1, mask: false, payloadData: '', }) => { try { if (!params.response?.opcode) { return params.response } if (params.response?.opcode === 1) { return params.response.payloadData } const binaryData = Buffer.from(params.response.payloadData, 'base64') console.info('结果解析:', decode(binaryData)) } catch (e) { console.error(e) } } Network.webSocketCreated(params => { console.log('WebSocket created:', params.url) }) // 订阅 Network.webSocketFrameSent 和 Network.webSocketFrameReceived 事件,监听 WebSocket 帧的发送和接收: Network.webSocketFrameSent(params => { console.log('WebSocket frame sent:', params.response) // formatResponse(params) }) Network.webSocketFrameReceived(params => { console.log('WebSocket frame received:', params.response) // formatResponse(params) }) // 订阅 Network.webSocketClosed 事件,监听 WebSocket 连接的关闭: Network.webSocketClosed(params => { console.log('WebSocket closed:', params.closeCode, params.closeReason) // formatResponse(params) }) // 订阅Network.webSocketWillSendHandshakeRequest事件,拦截WebSocket连接的握手请求: Network.webSocketWillSendHandshakeRequest(params => { console.log('WebSocket handshake request:', params.request) // formatResponse(params) }) // 订阅Network.webSocketHandshakeResponseReceived事件,拦截WebSocket连接的握手响应: Network.webSocketHandshakeResponseReceived(params => { console.log('WebSocket handshake response:', params.response) let data = formatResponse(params) console.info('webSocketHandshakeResponseReceived解析结果:', data) }) // 订阅Network.webSocketFrameSent和Network.webSocketFrameReceived事件,拦截WebSocket数据帧的发送和接收: Network.webSocketFrameSent(params => { console.log('WebSocket frame sent:', params.response) // formatResponse(params) }) Network.webSocketFrameReceived(params => { // console.log('WebSocket frame received:', params.response) console.info('webSocketFrameReceived 回复', params.response) let data = formatResponse(params) console.info('webSocketFrameReceived解析结果:', data) }) Network.webSocketFrameError(params => { console.log('WebSocket frame error:', params.response) let data = formatResponse(params) console.info('webSocketFrameError解析结果:', data) }) await sleep(timeout) done() }, timeout) test('拆包', done => { let str = `0{"sid":"b6535f35-3047-4f12-9cd3-c526b39cd019","upgrades":["websocket"],"pingInterval":30000,"pingTimeout":60000}` while (/^\d/.test(str)) { str = str.substring(1) } try { JSON.parse(str) } catch (e) { console.error(e) } console.info(str) done() }) })