From 70194ffedb8b8a7f2ff2cc39f28860bb31fe4cd2 Mon Sep 17 00:00:00 2001 From: zmzimpl Date: Sat, 20 Apr 2024 15:50:37 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=20cookie=20=E5=AF=BC?= =?UTF-8?q?=E5=85=A5=E5=8A=9F=E8=83=BD=EF=BC=8C=E4=BF=AE=E5=A4=8D=20proxy?= =?UTF-8?q?=20=E5=AF=BC=E5=85=A5=E5=B8=A6=E4=B8=8B=E5=88=92=E7=BA=BF=20=5F?= =?UTF-8?q?=20=E7=94=A8=E6=88=B7=E5=90=8D=E5=A4=B1=E8=B4=A5=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 14 +- package.json | 2 +- packages/main/src/fingerprint/index.ts | 65 +++---- packages/main/src/puppeteer/helpers.ts | 167 ++++++++++++++++++ packages/main/src/puppeteer/index.ts | 0 packages/main/src/puppeteer/tasks.ts | 0 packages/main/src/services/window-service.ts | 33 +++- packages/main/src/types/cookie.d.ts | 3 + packages/preload/src/bridges/window.ts | 5 + packages/renderer/src/constants/status.ts | 1 + packages/renderer/src/i18n.ts | 4 + .../renderer/src/pages/proxy/import/index.tsx | 2 +- .../windows/components/edit-form/index.tsx | 22 +-- packages/renderer/src/pages/windows/index.tsx | 50 +++++- packages/shared/types/db.d.ts | 6 +- pic.png | Bin 0 -> 372876 bytes 16 files changed, 309 insertions(+), 65 deletions(-) create mode 100644 packages/main/src/puppeteer/helpers.ts create mode 100644 packages/main/src/puppeteer/index.ts create mode 100644 packages/main/src/puppeteer/tasks.ts create mode 100644 packages/main/src/types/cookie.d.ts create mode 100644 pic.png diff --git a/README.md b/README.md index 3f2bc87..fc36e3a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Chrome Power +![Visualization](pic.png) + --- 首款开源指纹浏览器。基于 Puppeteer、Electron、React 开发。 @@ -33,17 +35,25 @@ Chromium 源码修改请参考 [chrome-power-chromium](https://github.com/zmzimp - [x] 中英文支持 - [x] Puppeteer/Playwright/Selenium 接入 - [ ] Mac 安装支持 -- [ ] 支持 cookie 登录 +- [ ] 支持 cookie 导入 - [ ] 扩展程序管理 - [ ] 同步操作 - [ ] 自动化脚本 +## 关于 Linux,Mac 支持问题 + +因为本人没有相关测试环境,请有相关需求的朋友自行通过本地编译运行,本质上与打包安装是相同的,甚至更方便升级。 + +欢迎 Linux、Mac 用户完善打包功能提 PR + ## 本地运行/打包 +环境:Node v18.18.2, npm 9.8.1 + - 安装依赖 `npm i` - 手动解压代码目录下的 `Chrome-bin.zip`,注意只有一层目录 - 运行调试 `npm run watch` -- 打包部署 `npm run publish`,注意打包时要把开发环境停掉,不然会导致 sqlite3 的包打包不了 +- (非必要)打包部署 `npm run publish`,注意打包时要把开发环境停掉,不然会导致 sqlite3 的包打包不了 ## API 文档 diff --git a/package.json b/package.json index 8aa9018..4cb2dfc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "chrome-power", "description": "The first open source fingerprint browser.", - "version": "1.1.1", + "version": "1.1.3", "private": true, "author": { "email": "zmzimpl@gmail.com", diff --git a/packages/main/src/fingerprint/index.ts b/packages/main/src/fingerprint/index.ts index e4623a5..e1bb909 100644 --- a/packages/main/src/fingerprint/index.ts +++ b/packages/main/src/fingerprint/index.ts @@ -3,7 +3,6 @@ import {ProxyDB} from '../db/proxy'; import {WindowDB} from '../db/window'; // import {getChromePath} from './device'; import {BrowserWindow} from 'electron'; -import type {Page} from 'puppeteer'; import puppeteer from 'puppeteer'; import {execSync, spawn} from 'child_process'; import * as portscanner from 'portscanner'; @@ -23,6 +22,7 @@ import {getPort} from '../server/index'; import {randomFingerprint} from '../services/window-service'; import {bridgeMessageToUI, getClientPort, getMainWindow} from '../mainWindow'; import {Mutex} from 'async-mutex'; +import {modifyPageInfo, presetCookie} from '../puppeteer/helpers'; const mutex = new Mutex(); @@ -30,40 +30,13 @@ const logger = createLogger(WINDOW_LOGGER_LABEL); const HOST = '127.0.0.1'; -// const HomePath = app.getPath('userData'); -// console.log(HomePath); - -const attachFingerprintToPuppeteer = async (page: Page, ipInfo: IP) => { - page.on('framenavigated', async _msg => { - try { - const title = await page.title(); - if (!title.includes('By ChromePower')) { - await page.evaluate(title => { - document.title = title + ' By ChromePower'; - }, title); - } - - await page.setGeolocation({latitude: ipInfo.ll?.[0], longitude: ipInfo.ll?.[1]}); - await page.emulateTimezone(ipInfo.timeZone); - } catch (error) { - bridgeMessageToUI({ - type: 'error', - text: (error as {message: string}).message, - }); - logger.error(error); - } - }); - await page.evaluateOnNewDocument( - 'navigator.mediaDevices.getUserMedia = navigator.webkitGetUserMedia = navigator.mozGetUserMedia = navigator.getUserMedia = webkitRTCPeerConnection = RTCPeerConnection = MediaStreamTrack = undefined;', - ); -}; - async function connectBrowser( port: number, ipInfo: IP, windowId: number, openStartPage: boolean = true, ) { + const windowData = await WindowDB.getById(windowId); const browserURL = `http://${HOST}:${port}`; const {data} = await api.get(browserURL + '/json/version'); if (data.webSocketDebuggerUrl) { @@ -72,10 +45,22 @@ async function connectBrowser( defaultViewport: null, }); + if (!windowData.opened_at) { + await presetCookie(windowId, browser); + } + await WindowDB.update(windowId, { + status: 2, + port: port, + opened_at: db.fn.now() as unknown as string, + }); + browser.on('targetcreated', async target => { const newPage = await target.page(); if (newPage) { - await attachFingerprintToPuppeteer(newPage, ipInfo); + await newPage.waitForNavigation({waitUntil: 'networkidle0'}); + // await newPage.setRequestInterception(true); + // await pageRequestInterceptor(windowId, newPage); + await modifyPageInfo(windowId, newPage, ipInfo); } }); const pages = await browser.pages(); @@ -87,7 +72,7 @@ async function connectBrowser( ? pages?.[0] : await browser.newPage(); try { - await attachFingerprintToPuppeteer(page, ipInfo); + await modifyPageInfo(windowId, page, ipInfo); if (getClientPort() && openStartPage) { await page.goto( `http://localhost:${getClientPort()}/#/start?windowId=${windowId}&serverPort=${getPort()}`, @@ -122,7 +107,7 @@ const getAvailablePort = async () => { throw new Error('Failed to find a free port after multiple attempts'); }; -export async function openFingerprintWindow(id: number) { +export async function openFingerprintWindow(id: number, headless = false) { const release = await mutex.acquire(); try { const windowData = await WindowDB.getById(id); @@ -181,6 +166,7 @@ export async function openFingerprintWindow(id: number) { `--remote-debugging-port=${chromePort}`, `--user-data-dir=${windowDataDir}`, `--user-agent=${fingerprint?.ua}`, + '--unhandled-rejections=strict', // below is for debug // '--enable-logging', // '--v=1', @@ -197,6 +183,10 @@ export async function openFingerprintWindow(id: number) { launchParamter.push(`--timezone=${ipInfo.timeZone}`); launchParamter.push(`--tz=${ipInfo.timeZone}`); } + if (headless) { + launchParamter.push('--headless'); + launchParamter.push('--disable-gpu'); + } let chromeInstance; try { chromeInstance = spawn(driverPath, launchParamter); @@ -207,11 +197,6 @@ export async function openFingerprintWindow(id: number) { return; } await sleep(1); - await WindowDB.update(id, { - status: 2, - port: chromePort, - opened_at: db.fn.now() as unknown as string, - }); win.webContents.send('window-opened', id); chromeInstance.stdout.on('data', _chunk => { // const str = _chunk.toString(); @@ -234,7 +219,7 @@ export async function openFingerprintWindow(id: number) { logger.info('Http Proxy server was closed.'); }); } - closeFingerprintWindow(id); + await closeFingerprintWindow(id); }); await sleep(1); @@ -244,7 +229,7 @@ export async function openFingerprintWindow(id: number) { } catch (error) { logger.error(error); execSync(`taskkill /PID ${chromeInstance.pid} /F`); - closeFingerprintWindow(id, true); + await closeFingerprintWindow(id, true); return null; } } else { @@ -314,7 +299,7 @@ export async function closeFingerprintWindow(id: number, force = false) { const window = await WindowDB.getById(id); const port = window.port; const status = window.status; - if (status === 2) { + if (status > 1) { if (force) { try { const browserURL = `http://${HOST}:${port}`; diff --git a/packages/main/src/puppeteer/helpers.ts b/packages/main/src/puppeteer/helpers.ts new file mode 100644 index 0000000..f1865e8 --- /dev/null +++ b/packages/main/src/puppeteer/helpers.ts @@ -0,0 +1,167 @@ +import type {Browser, Page} from 'puppeteer'; +import type {ICookie} from '../types/cookie'; +import {WindowDB} from '../db/window'; +import type {IP} from '../../../shared/types/ip'; +import {bridgeMessageToUI} from '../mainWindow'; + +type CookieDomain = string; + +const cookieMap: Map> = new Map(); + +// const cookieToMap = (windowId: number, cookies: ICookie[]) => { +// const map = new Map(); +// cookies.forEach(cookie => { +// console.log(cookie.domain); +// let domain; +// if (cookie.domain?.startsWith('.')) { +// domain = cookie.domain.slice(1); +// } +// if (!map.get(domain!)) { +// map.set(domain!, [cookie]); +// } else { +// const domainCookies = map.get(domain!); +// domainCookies?.push(cookie); +// map.set(domain!, domainCookies!); +// } +// }); +// cookieMap.set(windowId, map); +// }; + +const getCookie = (windowId: number, domain: CookieDomain) => { + const map = cookieMap.get(windowId); + if (map) { + return map.get(domain); + } + return null; +}; + +const parseCookie = (cookie: string) => { + // const correctedCookie = cookie.replace(/(\w+)(?=:)/g, '"$1"'); + try { + const jsonArray = JSON.parse(cookie); + return jsonArray; + } catch (error) { + console.error('解析错误:', error); + bridgeMessageToUI({ + type: 'error', + text: 'Cookie JSON 解析错误', + }); + } +}; + +export const setCookieToPage = async (windowId: number, page: Page) => { + const url = page.url(); + const urlObj = new URL(url); + const domain = urlObj.hostname; + const cookie = getCookie(windowId, domain); + const pageCookies = await page.cookies(); + console.log(domain, typeof pageCookies, pageCookies.length, cookie?.length); + if (!pageCookies.length) { + if (cookie?.length) { + console.log('set cookie:', cookie); + await page.setCookie(...cookie); + } + } +}; + +// 限流函数,限制同时执行的任务数 +// function limitConcurrency(maxConcurrentTasks: number) { +// let activeTasks = 0; +// const taskQueue: (() => Promise)[] = []; + +// function next() { +// if (activeTasks < maxConcurrentTasks && taskQueue.length > 0) { +// activeTasks++; +// const task = taskQueue.shift(); +// task!().finally(() => { +// activeTasks--; +// next(); +// }); +// } +// } + +// return (task: () => Promise) => { +// taskQueue.push(task); +// next(); +// }; +// } + +export const presetCookie = async (windowId: number, browser: Browser) => { + const window = await WindowDB.getById(windowId); + if (window?.cookie) { + if (typeof window.cookie === 'string') { + const correctedCookie = parseCookie(window.cookie); + const page = await browser.newPage(); + const client = await page.target().createCDPSession(); + await client.send('Network.enable'); + await client.send('Network.setCookies', { + cookies: correctedCookie, + }); + await page.close(); + // cookieToMap(windowId, correctedCookie || []); + // const runTask = limitConcurrency(10); // 最多同时执行 10 个任务 + + // const cookieTasks: Promise[] = []; + // const cookiesMap = cookieMap.get(windowId); + + // if (cookiesMap) { + // cookiesMap.forEach((cookies, domain) => { + // cookieTasks.push( + // new Promise(resolve => { + // runTask(async () => { + // const page = await browser.newPage(); + // // 获取 CDP 会话 + // const client = await page.target().createCDPSession(); + // try { + // await client.send('Network.enable'); + // await client.send('Network.setCookies', { + // cookies: cookies, + // }); + // } catch (error) { + // console.log(domain, 'set cookie error:', error); + // } finally { + // await page.close(); + // } + // resolve(); + // }); + // }), + // ); + // }); + // } + + // await Promise.all(cookieTasks); + // } + } + } + return true; +}; + +// export const pageRequestInterceptor = async (windowId: number, page: Page) => { +// const url = page.url(); +// const urlObj = new URL(url); +// page.on('request', async request => { + +// request.continue(); +// }); +// }; + +export const modifyPageInfo = async (windowId: number, page: Page, ipInfo: IP) => { + page.on('framenavigated', async _msg => { + try { + const title = await page.title(); + if (!title.includes('By ChromePower')) { + await page.evaluate(title => { + document.title = title + ' By ChromePower'; + }, title); + } + + await page.setGeolocation({latitude: ipInfo.ll?.[0], longitude: ipInfo.ll?.[1]}); + await page.emulateTimezone(ipInfo.timeZone); + } catch (error) { + console.error(error); + } + }); + await page.evaluateOnNewDocument( + 'navigator.mediaDevices.getUserMedia = navigator.webkitGetUserMedia = navigator.mozGetUserMedia = navigator.getUserMedia = webkitRTCPeerConnection = RTCPeerConnection = MediaStreamTrack = undefined;', + ); +}; diff --git a/packages/main/src/puppeteer/index.ts b/packages/main/src/puppeteer/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/main/src/puppeteer/tasks.ts b/packages/main/src/puppeteer/tasks.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/main/src/services/window-service.ts b/packages/main/src/services/window-service.ts index d877eb5..f501687 100644 --- a/packages/main/src/services/window-service.ts +++ b/packages/main/src/services/window-service.ts @@ -10,6 +10,8 @@ import {createLogger} from '../../../shared/utils/logger'; import {SERVICE_LOGGER_LABEL} from '../constants'; import {randomASCII, randomFloat, randomInt} from '../../../shared/utils'; import path from 'path'; +import puppeteer from 'puppeteer'; +import {presetCookie} from '../puppeteer/helpers'; const logger = createLogger(SERVICE_LOGGER_LABEL); export const initWindowService = () => { @@ -35,7 +37,14 @@ export const initWindowService = () => { }); ipcMain.handle('window-create', async (_, window: DB.Window, fingerprint: SafeAny) => { - logger.info('try to create window', JSON.stringify(window), JSON.stringify(fingerprint)); + logger.info( + 'try to create window', + JSON.stringify({ + ...window, + cookie: window?.cookie ? `preset ${window.cookie.length} cookies` : [], + }), + JSON.stringify(fingerprint), + ); return await WindowDB.create(window, fingerprint); }); @@ -84,10 +93,30 @@ export const initWindowService = () => { ipcMain.handle('window-close', async (_, id: number) => { return await closeFingerprintWindow(id, true); }); + + ipcMain.handle('window-set-cookie', async (_, id: number) => { + await WindowDB.update(id, { + status: 3, + }); + const {webSocketDebuggerUrl} = await openFingerprintWindow(id, true); + const browser = await puppeteer.connect({ + browserWSEndpoint: webSocketDebuggerUrl, + defaultViewport: null, + }); + await presetCookie(id, browser); + await browser.close(); + return { + success: true, + message: 'Set cookie successfully.', + }; + }); }; export const randomFingerprint = () => { - const uaPath = path.join(import.meta.env.MODE === 'development' ? 'assets' : 'resources/app/assets', 'ua.txt'); + const uaPath = path.join( + import.meta.env.MODE === 'development' ? 'assets' : 'resources/app/assets', + 'ua.txt', + ); const uaFile = readFileSync(uaPath, 'utf-8'); const uaList = uaFile.split('\n'); const randomIndex = Math.floor(Math.random() * uaList.length); diff --git a/packages/main/src/types/cookie.d.ts b/packages/main/src/types/cookie.d.ts new file mode 100644 index 0000000..bc567b4 --- /dev/null +++ b/packages/main/src/types/cookie.d.ts @@ -0,0 +1,3 @@ +import type {Protocol} from 'puppeteer'; + +export type ICookie = Protocol.Network.CookieParam \ No newline at end of file diff --git a/packages/preload/src/bridges/window.ts b/packages/preload/src/bridges/window.ts index 21b8146..fa0c1c7 100644 --- a/packages/preload/src/bridges/window.ts +++ b/packages/preload/src/bridges/window.ts @@ -56,6 +56,11 @@ export const WindowBridge = { return result; }, + async toogleSetCookie(id: number) { + const result = await ipcRenderer.invoke('window-set-cookie', id); + return result; + }, + onWindowClosed: (callback: (event: IpcRendererEvent, id: number) => void) => ipcRenderer.on('window-closed', callback), diff --git a/packages/renderer/src/constants/status.ts b/packages/renderer/src/constants/status.ts index 7ecc6b3..0a1e2cf 100644 --- a/packages/renderer/src/constants/status.ts +++ b/packages/renderer/src/constants/status.ts @@ -2,4 +2,5 @@ export const WINDOW_STATUS = { DEPRECATED: 0, NORMAL: 1, RUNNING: 2, + PREPARING: 3, }; diff --git a/packages/renderer/src/i18n.ts b/packages/renderer/src/i18n.ts index c11cbf0..c629cf9 100644 --- a/packages/renderer/src/i18n.ts +++ b/packages/renderer/src/i18n.ts @@ -32,7 +32,9 @@ i18n window_edit: 'Edit', window_delete: 'Delete', window_proxy_setting: 'Proxy setting', + window_set_cookie: 'Preset cookie', window_running: 'Running', + window_preparing: 'Preparing', window_column_profile_id: 'Profile ID', window_column_proxy: 'Proxy', window_column_group: 'Group', @@ -134,7 +136,9 @@ i18n window_edit: '编辑', window_delete: '删除', window_proxy_setting: '代理设置', + window_set_cookie: '预设 cookie', window_running: '运行中', + window_preparing: '配置中', window_column_profile_id: '缓存目录', window_column_proxy: '代理', window_column_group: '分组', diff --git a/packages/renderer/src/pages/proxy/import/index.tsx b/packages/renderer/src/pages/proxy/import/index.tsx index c4bd4e2..0cf8e1c 100644 --- a/packages/renderer/src/pages/proxy/import/index.tsx +++ b/packages/renderer/src/pages/proxy/import/index.tsx @@ -186,7 +186,7 @@ const ProxyImport = () => { } // 调整正则表达式以使用户名和密码可选 - const proxyRegex = /^([a-zA-Z0-9.-]+):(\d{1,5})(?::([a-zA-Z0-9-]*):([a-zA-Z0-9]*))?$/; + const proxyRegex = /^([a-zA-Z0-9.-]+):(\d{1,5})(?::([a-zA-Z0-9._-]*):([a-zA-Z0-9._-]*))?$/; if (!proxyRegex.test(proxy)) { throw new Error('无效的代理格式'); diff --git a/packages/renderer/src/pages/windows/components/edit-form/index.tsx b/packages/renderer/src/pages/windows/components/edit-form/index.tsx index 8250781..583d0f8 100644 --- a/packages/renderer/src/pages/windows/components/edit-form/index.tsx +++ b/packages/renderer/src/pages/windows/components/edit-form/index.tsx @@ -135,16 +135,6 @@ const WindowEditForm = ({ /> */} - {/* - name="cookie" - label="Cookie" - > -