Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
e9f64ae
Remove deprecated pkg_resources references
paveldudka Mar 7, 2024
16e0064
Update README.md
KrAsH-CoD3 Apr 14, 2024
32da068
Add screenshot for stealth with args
KrAsH-CoD3 Apr 14, 2024
bb8af62
Update navigator.userAgent.js
erma0 Apr 19, 2024
3904ba9
Update navigator.platform.js
erma0 Apr 19, 2024
eab6db9
Merge pull request #1 from paveldudka/pasha/pkg_resource
erma0 Apr 19, 2024
8a0fd59
Update chrome.csi.js
erma0 Apr 19, 2024
0c9e548
Merge pull request #2 from KrAsH-CoD3/Update-README
erma0 Apr 19, 2024
d195b81
Update navigator.userAgent.js
erma0 Apr 19, 2024
0fa38b1
Update navigator.userAgent.js
erma0 Apr 19, 2024
0ec23c7
adding user agent data evasion
iloveitaly May 17, 2024
3a88e5f
typo and other light changes
iloveitaly May 30, 2024
6df74fc
removing unneeded console logs
iloveitaly May 31, 2024
1ce348c
more accurate user agent data mock
iloveitaly May 31, 2024
d8fce6b
Merge remote-tracking branch 'iloveitaly/user-agent-fix'
alexstewartja Sep 10, 2024
cf53504
Merge remote-tracking branch 'iloveitaly/custom'
alexstewartja Sep 10, 2024
1f5e5d6
fix for opts
MaxxRK Sep 20, 2024
6f350c0
another try
MaxxRK Sep 20, 2024
a904622
try some more
MaxxRK Sep 20, 2024
7e53ad4
ugh
MaxxRK Sep 20, 2024
6458802
ugh2
MaxxRK Sep 20, 2024
ccc479b
bump version fix opts var
MaxxRK Sep 20, 2024
4810f76
require playwright vers
MaxxRK Sep 20, 2024
a6688bb
add more to tests.
MaxxRK Sep 20, 2024
a63e25e
require playwright version
MaxxRK Oct 4, 2024
2e11eee
get ready for release
MaxxRK Oct 22, 2024
ddc2dee
rename
MaxxRK Oct 22, 2024
635240f
fix py3.13 bump version
MaxxRK Oct 22, 2024
27eb295
fix depreceated build method
MaxxRK Oct 22, 2024
f87d7c3
bump requirements playwright
MaxxRK Oct 22, 2024
6485602
update playwright
MaxxRK Jun 17, 2025
21a5db8
update setup
MaxxRK Nov 6, 2025
dbd9869
update ignore
MaxxRK Nov 6, 2025
b852f50
update readme
MaxxRK Nov 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# These are supported funding model platforms

github: Maxxrk # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi:
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
41 changes: 41 additions & 0 deletions .github/workflows/python-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# This workflow will upload a Python Package using Twine when a release is created
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries

# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.

name: Upload Python Package

on:
release:
types: [published]

permissions:
contents: read

jobs:
deploy:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- name: Print commit SHA
run: echo ${{ github.sha }}
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build
- name: Build package
run: python -m build
- name: Publish package
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ wheels/
.installed.cfg
*.egg
MANIFEST
.pypirc

# PyInstaller
# Usually these files are written by a python script from a template
Expand Down Expand Up @@ -104,4 +105,4 @@ venv.bak/
.mypy_cache/

.vscode/
.idea/
.idea/
91 changes: 76 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,32 @@
# playwright_stealth

Transplanted from [puppeteer-extra-plugin-stealth](https://github.com/berstend/puppeteer-extra/tree/master/packages/puppeteer-extra-plugin-stealth), **Not perfect**.
Forked from [playwright_stealth](https://github.com/AtuboDad/playwright_stealth) and re-released.

## Install

```
$ pip install playwright-stealth
$ pip install playwright-sm
```

## Usage

Default stealth
### sync
```python

from playwright.sync_api import sync_playwright
from playwright_stealth import stealth_sync

with sync_playwright() as p:
for browser_type in [p.chromium, p.firefox, p.webkit]:
browser = browser_type.launch()
browser = browser_type.launch(headless=False)
page = browser.new_page()
stealth_sync(page)
page.goto('http://whatsmyuseragent.org/')
page.screenshot(path=f'example-{browser_type.name}.png')
page.goto('https://bot.sannysoft.com/')
page.screenshot(path=f'example-{browser_type.name}.png', full_page=True)
browser.close()

```

### async
```python
# -*- coding: utf-8 -*-
Expand All @@ -35,22 +37,81 @@ from playwright_stealth import stealth_async
async def main():
async with async_playwright() as p:
for browser_type in [p.chromium, p.firefox, p.webkit]:
browser = await browser_type.launch()
browser = await browser_type.launch(headless=False)
page = await browser.new_page()
await stealth_async(page)
await page.goto('http://whatsmyuseragent.org/')
await page.screenshot(path=f'example-{browser_type.name}.png')
await page.goto('https://bot.sannysoft.com/')
await page.screenshot(path=f'example-{browser_type.name}.png', full_page=True)
await browser.close()

asyncio.get_event_loop().run_until_complete(main())
asyncio.run(main())
```

## Test results
Desired stealth argument (as a mobile device)
### sync
```python
from playwright.sync_api import sync_playwright
from playwright_stealth import stealth_sync, StealthConfig

### playwright with stealth
with sync_playwright() as p:
for browser_type in [p.chromium, p.firefox, p.webkit]:
browser = browser_type.launch(headless=False)
context = browser.new_context(**p.devices["Pixel 7"])
page = context.new_page()
# Setting desired values for navigator properties
stealth_config = StealthConfig(
languages = ['en-US', 'en'],
navigator_plugins = False, # Mimicking real mobile device
navigator_hardware_concurrency = 8,
# nav_vendor = "", # Use only if you need to set empty string value to mimicking Firefox browser
nav_platform= 'Linux armv81',
vendor = 'Google Inc. (Qualcomm)',
renderer = 'ANGLE (Qualcomm, Adreno (TM) 640, OpenGL ES 3.2)',
)
stealth_sync(page, stealth_config)
page.goto('https://bot.sannysoft.com/')
page.screenshot(path=f'example-{browser_type.name}.png', full_page=True)
browser.close()
```

![playwright without stealth](./images/example_with_stealth.png)
### async
```python
# -*- coding: utf-8 -*-
import asyncio
from playwright.async_api import async_playwright
from playwright_stealth import stealth_async, StealthConfig

async def main():
async with async_playwright() as p:
for browser_type in [p.chromium, p.firefox, p.webkit]:
browser = await browser_type.launch(headless=False)
context = await browser.new_context(**p.devices["Pixel 7"])
page = await context.new_page()
# Setting desired values for navigator properties
stealth_config = StealthConfig(
languages = ['en-US', 'en'],
navigator_plugins = False, # Mimicking real mobile device
navigator_hardware_concurrency = 8,
# nav_vendor = "", # Use only if you need to set empty string value to mimicking Firefox browser
nav_platform= 'Linux armv81',
vendor = 'Google Inc. (Qualcomm)',
renderer = 'ANGLE (Qualcomm, Adreno (TM) 640, OpenGL ES 3.2)',
)
await stealth_async(page, stealth_config)
await page.goto('https://bot.sannysoft.com/')
await page.screenshot(path=f'example-{browser_type.name}.png')
await browser.close()

asyncio.run(main())
```

## Test results
### Playwright with stealth(no passed argument)
![playwright with stealth](./images/example_with_stealth.png)

### playwright without stealth
### Playwright without stealth
![playwright without stealth](./images/example_without_stealth.png)

![playwright with stealth](./images/example_without_stealth.png)
### Playwright with stealth(with passed argument) but as a mobile device
*NB: Mobile device have no plugin unlike desktop*
![playwright stealth with specified arguments](./images/example_with_stealth_passed_arguments.png)
Binary file added images/example_with_stealth_passed_arguments.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions playwright_stealth/js/chrome.csi.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ if (!window.chrome) {
if (!('csi' in window.chrome) && (window.performance || window.performance.timing)) {
const {csi_timing} = window.performance

log.info('loading chrome.csi.js')
// log.info('loading chrome.csi.js')
window.chrome.csi = function () {
return {
onloadT: csi_timing.domContentLoadedEventEnd,
Expand All @@ -24,4 +24,4 @@ if (!('csi' in window.chrome) && (window.performance || window.performance.timin
}
}
utils.patchToString(window.chrome.csi)
}
}
4 changes: 2 additions & 2 deletions playwright_stealth/js/navigator.platform.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
if (opts.navigator_platform) {
Object.defineProperty(Object.getPrototypeOf(navigator), 'platform', {
get: () => opts.navigator_plaftorm,
get: () => opts.navigator_platform,
})
}
}
2 changes: 1 addition & 1 deletion playwright_stealth/js/navigator.userAgent.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// replace Headless references in default useragent
const current_ua = navigator.userAgent
Object.defineProperty(Object.getPrototypeOf(navigator), 'userAgent', {
get: () => opts.navigator_user_agent || current_ua.replace('HeadlessChrome/', 'Chrome/')
get: () => window.opts.navigator_user_agent || current_ua.replace(/Headless/g, '')
})
111 changes: 111 additions & 0 deletions playwright_stealth/js/navigator.userAgentData.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
const ua = navigator.userAgent

// const uaVersion = ua.includes('Chrome/')
// ? ua.match(/Chrome\/([\d|.]+)/)[1]
// : (await page.browser().version()).match(/\/([\d|.]+)/)[1]
const uaVersion = ua.match(/Chrome\/([\d|.]+)/)[1]

// Source in C++: https://source.chromium.org/chromium/chromium/src/+/master:components/embedder_support/user_agent_utils.cc;l=55-100
const _getBrands = () => {
// const seed = uaVersion.split('.')[0] // the major version number of Chrome
const seed = 124

const order = [
[0, 1, 2],
[0, 2, 1],
[1, 0, 2],
[1, 2, 0],
[2, 0, 1],
[2, 1, 0]
][seed % 6]
const escapedChars = [' ', ' ', ';']

const greaseyBrand = `${escapedChars[order[0]]}Not${
escapedChars[order[1]]
}A${escapedChars[order[2]]}Brand`

const greasedBrandVersionList = []
greasedBrandVersionList[order[0]] = {
brand: greaseyBrand,
version: '99'
}
greasedBrandVersionList[order[1]] = {
brand: 'Chromium',
version: seed
}
greasedBrandVersionList[order[2]] = {
brand: 'Google Chrome',
version: seed
}

return greasedBrandVersionList
}

const metadata = {
platform: "macOS",
mobile: false,
brands: _getBrands()
}

const highEntropyValues = {
"architecture": "arm",
"bitness": "64",
"brands": [
{
"brand": "Chromium",
"version": "124"
},
{
"brand": "Google Chrome",
"version": "124"
},
{
"brand": "Not-A.Brand",
"version": "99"
}
],
"fullVersionList": [
{
"brand": "Chromium",
"version": "124.0.6367.208"
},
{
"brand": "Google Chrome",
"version": "124.0.6367.208"
},
{
"brand": "Not-A.Brand",
"version": "99.0.0.0"
}
],
"mobile": false,
"model": "",
"platform": "macOS",
"platformVersion": "14.4.1",
"uaFullVersion": "124.0.6367.208"
}

const userAgentMetadata = {
...metadata,
}

Object.defineProperty(userAgentMetadata, 'getHighEntropyValues', {
value: function(hints) {
return new Promise((resolve, reject) => {
let result = {};

hints.forEach(hint => {
if (highEntropyValues[hint]) {
result[hint] = highEntropyValues[hint];
}
});

resolve(result);
});
},
enumerable: false
});

Object.defineProperty(Object.getPrototypeOf(navigator), 'userAgentData', {
get: () => userAgentMetadata
})
1 change: 0 additions & 1 deletion playwright_stealth/js/webgl.vendor.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
console.log(opts)
const getParameterProxyHandler = {
apply: function (target, ctx, args) {
const param = (args || [])[0]
Expand Down
2 changes: 0 additions & 2 deletions playwright_stealth/js/window.outerdimensions.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ try {
if (!!window.outerWidth && !!window.outerHeight) {
const windowFrame = 85 // probably OS and WM dependent
window.outerWidth = window.innerWidth
console.log(`current window outer height ${window.outerHeight}`)
window.outerHeight = window.innerHeight + windowFrame
console.log(`new window outer height ${window.outerHeight}`)
}
} catch (err) {
}
Loading