Skip to content

Commit

Permalink
支持 cookie 导入功能,修复 proxy 导入带下划线 _ 用户名失败 bug
Browse files Browse the repository at this point in the history
  • Loading branch information
niuniuland committed Apr 20, 2024
1 parent 7c8d517 commit 70194ff
Show file tree
Hide file tree
Showing 16 changed files with 309 additions and 65 deletions.
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Chrome Power

![Visualization](pic.png)

---

首款开源指纹浏览器。基于 Puppeteer、Electron、React 开发。
Expand Down Expand Up @@ -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 文档

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": "[email protected]",
Expand Down
65 changes: 25 additions & 40 deletions packages/main/src/fingerprint/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -23,47 +22,21 @@ 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();

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) {
Expand All @@ -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();
Expand All @@ -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()}`,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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',
Expand All @@ -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);
Expand All @@ -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();
Expand All @@ -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);
Expand All @@ -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 {
Expand Down Expand Up @@ -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}`;
Expand Down
167 changes: 167 additions & 0 deletions packages/main/src/puppeteer/helpers.ts
Original file line number Diff line number Diff line change
@@ -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<number, Map<CookieDomain, ICookie[]>> = new Map();

// const cookieToMap = (windowId: number, cookies: ICookie[]) => {
// const map = new Map<CookieDomain, ICookie[]>();
// 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<void>)[] = [];

// function next() {
// if (activeTasks < maxConcurrentTasks && taskQueue.length > 0) {
// activeTasks++;
// const task = taskQueue.shift();
// task!().finally(() => {
// activeTasks--;
// next();
// });
// }
// }

// return (task: () => Promise<void>) => {
// 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<void>[] = [];
// const cookiesMap = cookieMap.get(windowId);

// if (cookiesMap) {
// cookiesMap.forEach((cookies, domain) => {
// cookieTasks.push(
// new Promise<void>(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;',
);
};
Empty file.
Empty file.
Loading

0 comments on commit 70194ff

Please sign in to comment.