Metadata-Version: 2.4
Name: red-eye-browser
Version: 1.0.5
Summary: Anti-fingerprinting browser automation library built on a custom Firefox binary
Author-email: Rohit Prajapati <prajapatirohit8860@gmail.com>
License: MIT
Keywords: red-eye,red-eye-browser,anti-fingerprint,fingerprint-spoofing,browser-automation,stealth-browser,undetected-browser,anti-bot,bot-detection-bypass,web-scraping,headless-browser,canvas-fingerprint,webgl-spoof,proxy-browser,firefox-automation
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Software Development :: Testing
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: MacOS
Classifier: Operating System :: POSIX :: Linux
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: playwright>=1.40
Requires-Dist: typing_extensions

# Red-Eye Browser

Anti-fingerprinting browser automation library built on a custom Firefox 140 binary.

## What is Red-Eye?

Red-Eye is a custom Firefox fork that spoofs browser fingerprints based on a JSON profile.
The browser runs on Windows but can impersonate macOS, Linux, or Windows profiles —
showing only the fonts, GPU, timezone, locale, and navigator data defined in the profile.

## Fingerprints Spoofed

| Fingerprint | Details |
|---|---|
| **User-Agent** | Full UA string from profile |
| **Navigator** | `platform`, `hardwareConcurrency`, `deviceMemory`, `maxTouchPoints`, `languages`, `language` |
| **Screen** | `width`, `height`, `availWidth`, `availHeight`, `colorDepth`, `innerWidth`, `innerHeight`, `outerWidth`, `outerHeight` |
| **Fonts** | System fonts isolated — web sees only profile fonts (macOS 17/51, Linux 7-8/51, Windows 7-10/51 in CreepJS) |
| **WebGL** | `vendor`, `renderer`, `version`, `shadingLanguageVersion`, unmasked vendor/renderer, extension list |
| **Canvas** | Text metrics rounded to prevent font fingerprinting |
| **Timezone** | ICU-level override — affects `Intl.DateTimeFormat`, `Date`, all JS time APIs |
| **Locale** | `Accept-Language` header + `navigator.languages` consistent |
| **HTTP Headers** | `User-Agent` header matches `navigator.userAgent` |
| **CSS Media** | `prefers-color-scheme`, `pointer`, `hover` from profile |
| **Math.random** | Seed influenced by profile hash |
| **Worker Navigator** | Same spoofing as main thread |
| **SVG Text** | Metrics rounded (same as Canvas 2D) |

## Installation

Requires Python 3.10 or newer.

```bash
pip install red-eye-browser
redeye install 8860
```

`redeye install` requires an access code. If the `redeye` command is not on
your `PATH`, call the module directly:

```bash
python -m redeye.cli install 8860
python -m redeye.cli status
```

## Usage

### Async

```python
import asyncio
from redeye import AsyncRedEye

async def main():
    async with AsyncRedEye() as browser:
        page = await browser.new_page()
        await page.goto("https://example.com")

asyncio.run(main())
```

### Sync

```python
from redeye import RedEye

with RedEye() as browser:
    page = browser.new_page()
    page.goto("https://example.com")
```

### With Proxy

```python
from redeye import AsyncRedEye

async with AsyncRedEye(proxy={
    "server": "http://host:port",
    "username": "user",
    "password": "pass"
}) as browser:
    page = await browser.new_page()
    await page.goto("https://example.com")
```

### Persistent Context

```python
import tempfile
from redeye import AsyncRedEye

async with AsyncRedEye(
    persistent_context=True,
    user_data_dir=tempfile.mkdtemp(),
    proxy={"server": "http://host:port", "username": "user", "password": "pass"}
) as context:
    page = context.pages[0]
    await page.goto("https://example.com")
```

### Launch with a Dynamic Profile

A complete script that picks a random profile, aligns the profile timezone
with the proxy exit IP, and leaves the browser open. Set
`REDEYE_PROFILES_DIR` to use your own profile directory; otherwise the
profiles bundled with the package are used.

```python
"""Launch Red-Eye with a randomly selected fingerprint profile."""

import asyncio
import json
import logging
import os
import random
import sys
import tempfile
import urllib.request
from pathlib import Path
from typing import Any, Dict, Optional, Tuple

import redeye
from redeye import AsyncRedEye

# Returns the timezone of whatever IP the request exits from, so a proxy's
# real location can be written into the profile.
GEO_URL = "http://ip-api.com/json/?fields=status,timezone"

PACKAGE_PROFILES_DIR = Path(redeye.__file__).resolve().parent / "profiles"
PROFILES_DIR = Path(
    os.environ.get("REDEYE_PROFILES_DIR") or PACKAGE_PROFILES_DIR
)

OS_TYPE = "windows"  # "linux", "windows" or "macos"
URL = "https://www.google.com/"

# Set to True to route the browser through a proxy. Each line of
# proxy_list.txt must be "host:port:username:password".
USE_PROXY = False
PROXY_LIST_PATH = Path(__file__).resolve().parent / "proxy_list.txt"

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(name)s %(message)s",
)
logger = logging.getLogger(__name__)


def get_random_profile(os_type: str = OS_TYPE) -> Dict[str, Any]:
    """Return a random profile JSON for the given operating system."""
    profiles = list(PROFILES_DIR.glob(f"*{os_type}*.json"))
    if not profiles:
        raise FileNotFoundError(
            f"No profile JSONs found for os_type={os_type!r} "
            f"in {PROFILES_DIR}"
        )
    chosen = random.choice(profiles)
    logger.info("Profile: %s", chosen.name)
    return json.loads(chosen.read_text(encoding="utf-8"))


def pick_proxy() -> Tuple[str, str, str, str]:
    """Return a random (host, port, username, password) from the proxy list."""
    lines = [
        line.strip()
        for line in PROXY_LIST_PATH.read_text(encoding="utf-8").splitlines()
        if line.strip()
    ]
    if not lines:
        raise ValueError(f"No proxies found in {PROXY_LIST_PATH}")
    host, port, username, password = random.choice(lines).split(":")
    logger.info("Proxy: %s:%s", host, port)
    return host, port, username, password


def get_proxy_timezone(
    host: str, port: str, username: str, password: str
) -> Optional[str]:
    """Look up the timezone of the proxy's exit IP, or None on failure."""
    proxy_url = f"http://{username}:{password}@{host}:{port}"
    opener = urllib.request.build_opener(
        urllib.request.ProxyHandler({"http": proxy_url})
    )
    try:
        request = urllib.request.Request(
            GEO_URL, headers={"User-Agent": "Mozilla/5.0"}
        )
        with opener.open(request, timeout=10) as response:
            data = json.loads(response.read().decode())
        if data.get("status") == "success" and data.get("timezone"):
            logger.info("Proxy timezone: %s", data["timezone"])
            return data["timezone"]
    except Exception as exc:  # network errors should not abort the launch
        logger.warning("Timezone fetch failed: %s", exc)
    return None


async def main(os_type: str = OS_TYPE) -> None:
    profile = get_random_profile(os_type)

    proxy_config: Optional[Dict[str, str]] = None
    if USE_PROXY:
        host, port, username, password = pick_proxy()
        # Align the profile timezone with the proxy's real location,
        # otherwise the geo mismatch is trivial to detect.
        timezone = get_proxy_timezone(host, port, username, password)
        if timezone:
            profile["timezone"] = timezone
        proxy_config = {
            "server": f"http://{host}:{port}",
            "username": username,
            "password": password,
        }

    async with AsyncRedEye(
        profile=profile,
        persistent_context=True,
        user_data_dir=tempfile.mkdtemp(),
        no_viewport=True,
        humanize=True,
        proxy=proxy_config,
    ) as browser:
        page = browser.pages[0]
        await page.goto(URL)
        logger.info("Launched: %s", page.url)

        # Keep the browser open until the user interrupts.
        while True:
            await asyncio.sleep(3600)


if __name__ == "__main__":
    requested_os = sys.argv[1] if len(sys.argv) > 1 else OS_TYPE
    try:
        asyncio.run(main(requested_os))
    except KeyboardInterrupt:
        logger.info("Closing browser")
```

Run it, optionally overriding the profile OS:

```bash
python launch_with_profile.py            # uses OS_TYPE
python launch_with_profile.py linux      # override
```

## CLI Commands

```bash
redeye install <code>            # Download the latest browser binary
redeye install <code> v1.0.4     # Install a specific release tag
redeye status                    # Show the installed version and path
redeye uninstall                 # Remove the browser binary
```

The version argument is the GitHub release tag, so it includes the leading
`v`. The access code is always the first argument to `install`.

## Profile System

Each profile is a JSON file containing fingerprint data:
- **Windows profiles** — impersonate Windows machine
- **macOS profiles** — impersonate macOS machine  
- **Linux profiles** — impersonate Linux machine

600 pre-built profiles included (200 per operating system). Custom profiles
supported — point `REDEYE_PROFILES_DIR` at your own directory, or pass a
path or dict to the `profile` argument.

## Author

Rohit Prajapati — prajapatirohit8860@gmail.com
