Skip to content

Commit bab5138

Browse files
committed
feat(examples): add Python per-context fingerprint example, rewrite proxy to use BotBrowser CDP commands
1 parent d37f212 commit bab5138

3 files changed

Lines changed: 326 additions & 102 deletions

File tree

examples/playwright/nodejs/per_context_fingerprint.js

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
* BOTBROWSER_EXEC_PATH=/path/to/chrome \
1919
* PROFILE_A=/path/to/profile-a.enc \
2020
* PROFILE_B=/path/to/profile-b.enc \
21+
* PROXY_B=socks5://user:pass@host:1080 \
2122
* node per_context_fingerprint.js
2223
*/
2324

@@ -27,12 +28,14 @@ const { chromium } = require('playwright-core');
2728
const execPath = process.env.BOTBROWSER_EXEC_PATH;
2829
const profileA = process.env.PROFILE_A;
2930
const profileB = process.env.PROFILE_B;
31+
const proxyB = process.env.PROXY_B; // optional, e.g. socks5://user:pass@host:1080
3032

3133
if (!execPath || !profileA || !profileB) {
3234
console.log('Usage:');
3335
console.log(' BOTBROWSER_EXEC_PATH=/path/to/chrome \\');
3436
console.log(' PROFILE_A=/path/to/profile-a.enc \\');
3537
console.log(' PROFILE_B=/path/to/profile-b.enc \\');
38+
console.log(' PROXY_B=socks5://user:pass@host:1080 \\');
3639
console.log(' node per_context_fingerprint.js');
3740
process.exit(1);
3841
}
@@ -77,14 +80,18 @@ const { chromium } = require('playwright-core');
7780
// 5. Now create the page. The renderer starts with the correct flags.
7881
const pageA = await ctxA.newPage();
7982

80-
// --- Context B: profile B ---
83+
// --- Context B: profile B + optional proxy ---
8184
const { browserContextIds: before2 } = await browserCDP.send('Target.getBrowserContexts');
8285
const ctxB = await browser.newContext();
8386
const { browserContextIds: after2 } = await browserCDP.send('Target.getBrowserContexts');
8487
const ctxIdB = after2.filter(id => !before2.includes(id))[0];
88+
89+
const flagsB = [`--bot-profile=${profileB}`];
90+
if (proxyB) flagsB.push(`--proxy-server=${proxyB}`);
91+
8592
await browserCDP.send('BotBrowser.setBrowserContextFlags', {
8693
browserContextId: ctxIdB,
87-
botbrowserFlags: [`--bot-profile=${profileB}`],
94+
botbrowserFlags: flagsB,
8895
});
8996
const pageB = await ctxB.newPage();
9097

@@ -104,6 +111,19 @@ const { chromium } = require('playwright-core');
104111
console.log('\nContext B fingerprint:');
105112
printFingerprint(fpB);
106113

114+
// Check proxy IP for context B (if proxy was set)
115+
if (proxyB) {
116+
console.log('\n--- Proxy check (Context B) ---');
117+
try {
118+
await pageB.goto('https://httpbin.org/ip', { timeout: 20000 });
119+
const ipBody = await pageB.evaluate(() => document.body.innerText);
120+
const ip = JSON.parse(ipBody).origin;
121+
console.log(` Context B exit IP: ${ip}`);
122+
} catch (e) {
123+
console.log(` Proxy check failed: ${e.message}`);
124+
}
125+
}
126+
107127
// Verify isolation
108128
console.log('\n--- Isolation check ---');
109129
const fields = ['userAgent', 'platform', 'hardwareConcurrency', 'screenWidth', 'webglRenderer'];
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
"""
2+
PRIVACY RESEARCH USE ONLY
3+
Run exclusively in authorized privacy research labs that comply with all applicable laws.
4+
See: https://github.com/botswin/BotBrowser/blob/main/DISCLAIMER.md
5+
6+
BotBrowser Per-Context Fingerprint + Proxy Example (Python Playwright)
7+
8+
Creates multiple browser contexts in a single browser instance, each with
9+
a different fingerprint profile and optional proxy via BotBrowser.setBrowserContextFlags.
10+
11+
Requirements:
12+
pip install playwright
13+
BotBrowser binary (not stock Chromium)
14+
At least two .enc profile files
15+
16+
Usage:
17+
BOTBROWSER_EXEC_PATH=/path/to/chrome \
18+
PROFILE_A=/path/to/profile-a.enc \
19+
PROFILE_B=/path/to/profile-b.enc \
20+
PROXY_B=socks5://user:pass@host:1080 \
21+
python per_context_fingerprint.py
22+
"""
23+
24+
import asyncio
25+
import json
26+
import os
27+
import sys
28+
29+
from playwright.async_api import async_playwright
30+
31+
32+
async def create_bb_context(browser, cdp_session, profile_path, proxy=None):
33+
"""
34+
Create a BrowserContext with a BotBrowser fingerprint profile and optional proxy.
35+
36+
Steps:
37+
1. Snapshot existing context IDs
38+
2. Create context via Playwright
39+
3. Find the new context ID by diffing before/after
40+
4. Set BotBrowser flags BEFORE creating any page
41+
"""
42+
# Step 1: snapshot
43+
result = await cdp_session.send("Target.getBrowserContexts")
44+
before_ids = result["browserContextIds"]
45+
46+
# Step 2: create context
47+
context = await browser.new_context()
48+
49+
# Step 3: find new context ID
50+
result = await cdp_session.send("Target.getBrowserContexts")
51+
after_ids = result["browserContextIds"]
52+
context_id = None
53+
for cid in after_ids:
54+
if cid not in before_ids:
55+
context_id = cid
56+
break
57+
58+
if not context_id:
59+
raise RuntimeError("Could not determine browserContextId")
60+
61+
# Step 4: set flags (profile + optional proxy)
62+
flags = [f"--bot-profile={profile_path}"]
63+
if proxy:
64+
flags.append(f"--proxy-server={proxy}")
65+
66+
await cdp_session.send("BotBrowser.setBrowserContextFlags", {
67+
"browserContextId": context_id,
68+
"botbrowserFlags": flags,
69+
})
70+
71+
return context, context_id
72+
73+
74+
async def collect_fingerprint(page):
75+
"""Collect key fingerprint signals from a page."""
76+
return await page.evaluate("""async () => {
77+
const r = {};
78+
r.userAgent = navigator.userAgent;
79+
r.platform = navigator.platform;
80+
r.hardwareConcurrency = navigator.hardwareConcurrency;
81+
r.deviceMemory = navigator.deviceMemory;
82+
r.screenWidth = screen.width;
83+
r.screenHeight = screen.height;
84+
r.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
85+
r.languages = JSON.stringify(navigator.languages);
86+
try {
87+
const c = document.createElement('canvas');
88+
const gl = c.getContext('webgl');
89+
if (gl) {
90+
const d = gl.getExtension('WEBGL_debug_renderer_info');
91+
if (d) r.webglRenderer = gl.getParameter(d.UNMASKED_RENDERER_WEBGL);
92+
}
93+
} catch (_) {}
94+
return r;
95+
}""")
96+
97+
98+
def print_fingerprint(label, fp):
99+
print(f"\n [{label}]")
100+
print(f" UA: {fp['userAgent'][:80]}")
101+
print(f" Platform: {fp['platform']}")
102+
print(f" HW: {fp['hardwareConcurrency']} cores, {fp.get('deviceMemory', 'N/A')}GB")
103+
print(f" Screen: {fp['screenWidth']}x{fp['screenHeight']}")
104+
print(f" WebGL: {fp.get('webglRenderer', 'N/A')}")
105+
print(f" Timezone: {fp['timezone']}")
106+
print(f" Langs: {fp.get('languages', 'N/A')}")
107+
108+
109+
async def main():
110+
exec_path = os.environ.get("BOTBROWSER_EXEC_PATH")
111+
profile_a = os.environ.get("PROFILE_A")
112+
profile_b = os.environ.get("PROFILE_B")
113+
proxy_b = os.environ.get("PROXY_B") # optional
114+
115+
if not exec_path or not profile_a or not profile_b:
116+
print("Usage:")
117+
print(" BOTBROWSER_EXEC_PATH=/path/to/chrome \\")
118+
print(" PROFILE_A=/path/to/profile-a.enc \\")
119+
print(" PROFILE_B=/path/to/profile-b.enc \\")
120+
print(" PROXY_B=socks5://user:pass@host:1080 \\")
121+
print(" python per_context_fingerprint.py")
122+
sys.exit(1)
123+
124+
async with async_playwright() as pw:
125+
# Launch with profile A as the base (default) profile
126+
browser = await pw.chromium.launch(
127+
executable_path=exec_path,
128+
headless=True,
129+
args=[
130+
"--disable-blink-features=AutomationControlled",
131+
"--disable-audio-output",
132+
f"--bot-profile={profile_a}",
133+
],
134+
)
135+
print("Browser launched")
136+
137+
cdp = await browser.new_browser_cdp_session()
138+
139+
try:
140+
# --- Context A: profile A (no proxy) ---
141+
ctx_a, ctx_id_a = await create_bb_context(browser, cdp, profile_a)
142+
page_a = await ctx_a.new_page()
143+
print(f"\n Context A created (id={ctx_id_a})")
144+
145+
# --- Context B: profile B + optional proxy ---
146+
ctx_b, ctx_id_b = await create_bb_context(browser, cdp, profile_b, proxy=proxy_b)
147+
page_b = await ctx_b.new_page()
148+
print(f" Context B created (id={ctx_id_b})")
149+
if proxy_b:
150+
print(f" Context B proxy: {proxy_b}")
151+
152+
# Navigate both
153+
await asyncio.gather(
154+
page_a.goto("https://example.com", wait_until="domcontentloaded"),
155+
page_b.goto("https://example.com", wait_until="domcontentloaded"),
156+
)
157+
158+
# Collect fingerprints
159+
fp_a = await collect_fingerprint(page_a)
160+
fp_b = await collect_fingerprint(page_b)
161+
162+
print_fingerprint("Context A", fp_a)
163+
print_fingerprint("Context B", fp_b)
164+
165+
# Check proxy IP for context B
166+
if proxy_b:
167+
print("\n --- Proxy check (Context B) ---")
168+
try:
169+
await page_b.goto("https://httpbin.org/ip", timeout=20000)
170+
body = await page_b.evaluate("() => document.body.innerText")
171+
ip = json.loads(body).get("origin", "N/A")
172+
print(f" Exit IP: {ip}")
173+
except Exception as e:
174+
print(f" Proxy check failed: {e}")
175+
176+
# Verify isolation
177+
print("\n --- Isolation check ---")
178+
fields = ["userAgent", "platform", "hardwareConcurrency", "screenWidth", "webglRenderer"]
179+
diffs = 0
180+
for f in fields:
181+
same = fp_a.get(f) == fp_b.get(f)
182+
if not same:
183+
diffs += 1
184+
print(f" {f}: {'SAME' if same else 'DIFFERENT'}")
185+
print(f"\n {diffs} of {len(fields)} fields differ between contexts.")
186+
187+
finally:
188+
await browser.close()
189+
190+
191+
if __name__ == "__main__":
192+
asyncio.run(main())

0 commit comments

Comments
 (0)