mirror of
https://github.com/ThePhaseless/Byparr.git
synced 2026-09-24 14:20:08 +01:00
Migrate to camoufox (#235)
* use camoufox * build custom branches * organize compose * ignore missing stubs * add init method * create ADDON PATH * check if solving is required * type checking and remove logger * uv sync * add timeout * add descriptiuon * cancel previous runs * wildcard minor * fix proxy * syntax fix * [skip ci] fix loop warning * fix proxy format * copy over docker ignore * remove testing line * comment out port expose * remove idope * use strict typechecking * refactor * adjust compose * run init only before test * try test without init * add curl * handle page response * remove turnsile test * fix dockerignore symlink * use newer debian * bump pytest * add docker in docker in devcontaienr * remove unused values * handle timeouts * fix edgecase * use new envs for proxy * remove init command * remove testing line * remove setuptools * fix linters * reimport * readd setuptools * better float handling
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
"dockerfile": "../Dockerfile",
|
||||
"target": "devcontainer"
|
||||
},
|
||||
"runArgs": ["--security-opt", "label=disable", "--privileged"],
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": ["charliermarsh.ruff", "ms-python.python"],
|
||||
@@ -14,5 +15,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"postCreateCommand": "uv sync --group test && cd .venv/lib/*/site-packages/seleniumbase/drivers && rm -f uc_driver && ln -s /usr/bin/chromedriver uc_driver"
|
||||
"postCreateCommand": "uv sync --group test",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {
|
||||
"moby": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ on:
|
||||
schedule:
|
||||
- cron: "25 0 * * *"
|
||||
push:
|
||||
branches: ["main"]
|
||||
branches: ["*"]
|
||||
# Publish semver tags as releases.
|
||||
tags: ["v*.*.*"]
|
||||
pull_request:
|
||||
|
||||
+4
-1
@@ -168,4 +168,7 @@ core
|
||||
screenshots/
|
||||
|
||||
# Downloaded files
|
||||
downloaded_files/
|
||||
downloaded_files/
|
||||
|
||||
# Browser Data
|
||||
browser_data/
|
||||
|
||||
Vendored
+2
@@ -1,10 +1,12 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"console": "integratedTerminal",
|
||||
"env": {
|
||||
"LOG_LEVEL": "DEBUG"
|
||||
},
|
||||
"purpose": ["debug-test", "debug-in-terminal"],
|
||||
"justMyCode": false,
|
||||
"name": "Python Debugger: Main",
|
||||
"program": "main.py",
|
||||
|
||||
Vendored
+17
-22
@@ -1,23 +1,18 @@
|
||||
{
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "charliermarsh.ruff"
|
||||
},
|
||||
"python.analysis.autoImportCompletions": true,
|
||||
"python.analysis.packageIndexDepths": [
|
||||
{
|
||||
"depth": 5,
|
||||
"includeAllSymbols": true,
|
||||
"name": ""
|
||||
}
|
||||
],
|
||||
"python.analysis.typeCheckingMode": "standard",
|
||||
"python.terminal.activateEnvironment": true,
|
||||
"python.testing.pytestArgs": [
|
||||
"tests",
|
||||
"-n",
|
||||
"auto",
|
||||
"--retries=3",
|
||||
],
|
||||
"python.testing.pytestEnabled": true,
|
||||
"python.testing.unittestEnabled": false
|
||||
}
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "charliermarsh.ruff"
|
||||
},
|
||||
"python.analysis.autoImportCompletions": true,
|
||||
"python.analysis.packageIndexDepths": [
|
||||
{
|
||||
"depth": 5,
|
||||
"includeAllSymbols": true,
|
||||
"name": ""
|
||||
}
|
||||
],
|
||||
"python.terminal.activateEnvironment": true,
|
||||
"python.testing.pytestArgs": ["tests", "-n", "auto", "--retries=3"],
|
||||
"python.testing.pytestEnabled": true,
|
||||
"python.testing.unittestEnabled": false,
|
||||
"cSpell.words": ["camoufox", "domcontentloaded", "networkidle"]
|
||||
}
|
||||
|
||||
+7
-12
@@ -1,4 +1,4 @@
|
||||
FROM debian:trixie-slim AS base
|
||||
FROM debian:stable-slim AS base
|
||||
ENV HOME=/root
|
||||
|
||||
ARG GITHUB_BUILD=false \
|
||||
@@ -10,21 +10,19 @@ ENV GITHUB_BUILD=${GITHUB_BUILD}\
|
||||
PYTHONUNBUFFERED=1 \
|
||||
# prevents python creating .pyc files
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
DISPLAY=:0\
|
||||
UV_LINK_MODE=copy \
|
||||
PATH="${HOME}/.local/bin:$PATH"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends --no-install-suggests xauth xvfb scrot curl chromium chromium-driver ca-certificates tini
|
||||
RUN apt update && apt -y upgrade && apt install -y curl
|
||||
|
||||
ADD https://astral.sh/uv/install.sh install.sh
|
||||
RUN sh install.sh && uv --version
|
||||
|
||||
RUN uvx playwright install-deps firefox && uvx camoufox fetch
|
||||
|
||||
FROM base AS devcontainer
|
||||
RUN apt install -y git && apt upgrade -y
|
||||
ENV UV_LINK_MODE=copy
|
||||
RUN apt install -y git
|
||||
ENTRYPOINT [ "sleep", "infinity" ]
|
||||
|
||||
|
||||
@@ -32,16 +30,13 @@ FROM base AS app
|
||||
COPY pyproject.toml uv.lock ./
|
||||
RUN --mount=type=cache,target=${HOME}/.cache/uv uv sync
|
||||
|
||||
# SeleniumBase does not come with an arm64 chromedriver binary
|
||||
RUN cd .venv/lib/*/site-packages/seleniumbase/drivers && rm -f uc_driver && ln -s /usr/bin/chromedriver uc_driver
|
||||
COPY . .
|
||||
|
||||
|
||||
FROM app AS test
|
||||
RUN --mount=type=cache,target=${HOME}/.cache/uv uv sync --group test
|
||||
RUN ./test.sh
|
||||
RUN uv run pytest --retries 3 -n auto
|
||||
|
||||
FROM app
|
||||
EXPOSE 8191
|
||||
HEALTHCHECK --interval=15m --timeout=30s --start-period=5s --retries=3 CMD [ "curl", "http://localhost:8191/health" ]
|
||||
ENTRYPOINT ["/usr/bin/tini", "--", "uv", "run", "main.py"]
|
||||
ENTRYPOINT ["uv", "run", "main.py"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Byparr
|
||||
|
||||
Built with [seleniumbase](https://seleniumbase.io/) and [FastAPI](https://fastapi.tiangolo.com), this project aims to mimic [FlareSolverr's](https://github.com/FlareSolverr/FlareSolverr) API and functionality of providing you with http cookies and headers for websites protected with anti-bot protections.
|
||||
Built with [camoufox](https://camoufox.com/) and [FastAPI](https://fastapi.tiangolo.com), this project aims to mimic [FlareSolverr's](https://github.com/FlareSolverr/FlareSolverr) API and functionality of providing you with http cookies and headers for websites protected with anti-bot protections.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> This software does not **guarantee** (only greatly increases the chance) that any challenge will be bypassed. While this tool passes the initial browser check, Cloudflare and other captcha providers likely require valid network traffic originating from the user’s public IP address to mark a connection as legitimate. If any website does not pass the challenge, please run troubleshooting steps and check if other websites work before you create an GitHub issue.
|
||||
@@ -33,18 +33,16 @@ Built with [seleniumbase](https://seleniumbase.io/) and [FastAPI](https://fastap
|
||||
|
||||
## Options
|
||||
|
||||
| Environment Variable | Default | Description |
|
||||
|----------------------|------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `HOST` | `0.0.0.0` | Host address to bind the server to. Use `0.0.0.0` to bind to all IPv4 interfaces, `::` for all IPv6 interfaces, or `127.0.0.1`/`localhost` for local access only. |
|
||||
| `PROXY` | None | Proxy to use in format: `protocol://username:password@host:port`. [SOCKS5 with authentication is not supported by Chrome](https://stackoverflow.com/questions/75602916/connection-to-private-proxy-socks5-with-chrome-webrequest-onauthrequired-and), see `compose.yaml` file for a workaround |
|
||||
| `USE_HEADLESS` | `SeleniumBase default` | Use headless chromium. |
|
||||
| `USE_XVFB` | `SeleniumBase default` | Use activate the special virtual display. |
|
||||
| `CAPTCHA_RETRIES` | 3 | Number of times to retry solving a CAPTCHA challenge before giving up. This helps in cases where CAPTCHA challenges fail intermittently and can be retried successfully. |
|
||||
| Environment Variable | Default | Description |
|
||||
| -------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `HOST` | `0.0.0.0` | Host address to bind the server to. Use `0.0.0.0` to bind to all IPv4 interfaces, `::` for all IPv6 interfaces, or `127.0.0.1`/`localhost` for local access only. |
|
||||
| `PROXY_SERVER` | None | Proxy to use in format: `protocol://host:port`. |
|
||||
| `PROXY_USERNAME` | None | |
|
||||
| `PROXY_PASSWORD` | None | |
|
||||
|
||||
## Proxy Recommendation
|
||||
|
||||
Recently I've partnered with a *new in town* proxy service - ProxyBase - to offer affordable proxy services that seems to work seamlessly with Byparr! Using my affiliate code `byparr` (case sensitive!) when signing up will not only get you access to their cost-effective (**$0.69/GB with occasional promotions**) proxy network but will also help support the continued development of this project. ProxyBase's proxies can significantly improve your success rate when bypassing anti-bot challenges. [Check out ProxyBase](https://client.proxybase.org/signup?ref=byparr
|
||||
) and enhance your Byparr experience!
|
||||
Recently I've partnered with a _new in town_ proxy service - ProxyBase - to offer affordable proxy services that seems to work seamlessly with Byparr! Using my affiliate code `byparr` (case sensitive!) when signing up will not only get you access to their cost-effective (**$0.69/GB with occasional promotions** _at the time of writing_) proxy network but will also help support the continued development of this project. ProxyBase's proxies can significantly improve your success rate when bypassing anti-bot challenges. [Check out ProxyBase](https://client.proxybase.org/signup?ref=byparr) and enhance your Byparr experience!
|
||||
|
||||
## Tags
|
||||
|
||||
@@ -60,7 +58,7 @@ See `compose.yaml`
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
docker run --shm-size=2gb -p 8191:8191 ghcr.io/thephaseless/byparr:latest
|
||||
docker run -p 8191:8191 ghcr.io/thephaseless/byparr:latest
|
||||
```
|
||||
|
||||
### Local
|
||||
|
||||
+1
-20
@@ -2,28 +2,9 @@ services:
|
||||
byparr:
|
||||
image: ghcr.io/thephaseless/byparr:latest
|
||||
restart: unless-stopped
|
||||
shm_size: 2gb
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
## Save screenshots when exception occurs
|
||||
# volumes:
|
||||
# - ./screenshots:/app/screenshots
|
||||
## Enable traffic outside of compose internal network
|
||||
# Uncomment below to use byparr outside of internal network
|
||||
# ports:
|
||||
# - "8191:8191"
|
||||
## Uncomment below to use proxy with unsupported protocol
|
||||
# environment:
|
||||
# - PROXY=http://pproxy.service:8080
|
||||
# pproxy:
|
||||
# tty: true # Required for pproxy to work
|
||||
# container_name: pproxy
|
||||
# hostname: pproxy.service # Selenium required a dot in hostname
|
||||
# restart: unless-stopped
|
||||
# image: mosajjal/pproxy:latest
|
||||
# command:
|
||||
# - "-vv"
|
||||
# - "-l"
|
||||
# - "http://:8080"
|
||||
# - "-r"
|
||||
# - "prot://host:port#username:password"
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
import uvicorn
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.gzip import GZipMiddleware
|
||||
|
||||
from src.consts import LOG_LEVEL, VERSION
|
||||
from src.endpoints import router
|
||||
from src.endpoints import health_check, router
|
||||
from src.middlewares import LogRequest
|
||||
from src.utils import logger
|
||||
from src.utils import get_camoufox, logger
|
||||
|
||||
logger.info("Using version %s", VERSION)
|
||||
|
||||
@@ -21,6 +23,20 @@ app.add_middleware(LogRequest)
|
||||
app.include_router(router=router)
|
||||
|
||||
|
||||
async def init():
|
||||
"""Initialize the application."""
|
||||
async for browser in get_camoufox():
|
||||
await health_check(browser)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
host = os.getenv("HOST", "0.0.0.0")
|
||||
uvicorn.run(app, host=host, port=8191, log_level=LOG_LEVEL) # noqa: S104
|
||||
# Check for --init flag to run the app in development mode
|
||||
if "--init" in sys.argv:
|
||||
logger.info("Running initialization script...")
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
loop.run_until_complete(init())
|
||||
logger.info("Initialization complete.")
|
||||
else:
|
||||
host = os.getenv("HOST", "0.0.0.0") # noqa: S104
|
||||
uvicorn.run(app, host=host, port=8191)
|
||||
|
||||
+13
-6
@@ -8,19 +8,22 @@ version = "0.1.0"
|
||||
description = "API for getting cookies for Cloudflare challenges"
|
||||
readme = "README.md"
|
||||
dependencies = [
|
||||
"camoufox[geoip]==0.4.*",
|
||||
"fastapi[standard]==0.116.*",
|
||||
"pyautogui==0.9.*",
|
||||
"playwright-captcha==0.1.*",
|
||||
"pydantic==2.11.*",
|
||||
"seleniumbase==4.41.3",
|
||||
]
|
||||
urls = { repository = "https://github.com/ThePhaseless/Byparr" }
|
||||
|
||||
[dependency-groups]
|
||||
test = [
|
||||
"httpx==0.28.1",
|
||||
"pytest-asyncio==1.1.0",
|
||||
"pytest-progress==1.3.0",
|
||||
"pytest-retry==1.7.0",
|
||||
"httpx==0.28.*",
|
||||
"pytest==8.4.*",
|
||||
"pytest-asyncio==1.1.*",
|
||||
"pytest-progress==1.3.*",
|
||||
"pytest-retry==1.7.*",
|
||||
"pytest-xdist==3.8.*",
|
||||
"setuptools>=80.9.0",
|
||||
]
|
||||
dev = ["deptry==0.23.*", "ruff==0.12.*"]
|
||||
|
||||
@@ -55,3 +58,7 @@ ignore = [
|
||||
]
|
||||
select = ["ALL"]
|
||||
extend-safe-fixes = ["D415"]
|
||||
|
||||
[tool.pyright]
|
||||
reportMissingTypeStubs = false
|
||||
typeCheckingMode = "strict"
|
||||
|
||||
+20
-37
@@ -1,46 +1,29 @@
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_version_from_env():
|
||||
"""
|
||||
Retrieve the version from the environment variable 'VERSION'.
|
||||
|
||||
This function checks the 'VERSION' environment variable for a value
|
||||
that starts with 'v' and returns the version without the prefix.
|
||||
|
||||
Returns:
|
||||
str | None: The version string without the 'v' prefix, or None if
|
||||
the 'VERSION' environment variable is not set or does not start
|
||||
with 'v'.
|
||||
|
||||
"""
|
||||
version_env = os.getenv("VERSION")
|
||||
if not version_env:
|
||||
return None
|
||||
|
||||
return version_env.removeprefix("v")
|
||||
|
||||
|
||||
LOG_LEVEL = os.getenv("LOG_LEVEL") or "INFO"
|
||||
LOG_LEVEL = logging.getLevelNamesMapping()[LOG_LEVEL.upper()]
|
||||
|
||||
VERSION = get_version_from_env() or "unknown"
|
||||
|
||||
USE_XVFB = os.getenv("USE_XVFB") in ["true", "1"] if os.getenv("USE_XVFB") else None
|
||||
|
||||
USE_HEADLESS = (
|
||||
os.getenv("USE_HEADLESS") in ["true", "1"] if os.getenv("USE_HEADLESS") else None
|
||||
from playwright_captcha import CaptchaType
|
||||
from playwright_captcha.utils.camoufox_add_init_script.add_init_script import (
|
||||
get_addon_path,
|
||||
)
|
||||
|
||||
CAPTCHA_RETRIES = int(os.getenv("CAPTCHA_RETRIES", "3"))
|
||||
LOG_LEVEL = logging.getLevelNamesMapping()[os.getenv("LOG_LEVEL", "INFO").upper()]
|
||||
|
||||
PROXY = os.getenv("PROXY")
|
||||
VERSION = os.getenv("VERSION", "unknown").removeprefix("v")
|
||||
|
||||
ADDON_PATH = str(Path(get_addon_path()).absolute())
|
||||
MAX_ATTEMPTS = 2**10
|
||||
|
||||
|
||||
PROXY_SERVER = os.getenv("PROXY_SERVER")
|
||||
PROXY_USERNAME = os.getenv("PROXY_USERNAME")
|
||||
PROXY_PASSWORD = os.getenv("PROXY_PASSWORD")
|
||||
|
||||
CHALLENGE_TITLES_MAP: dict[CaptchaType, list[str]] = {
|
||||
# Cloudflare
|
||||
CaptchaType.CLOUDFLARE_INTERSTITIAL: ["Just a moment..."],
|
||||
}
|
||||
|
||||
CHALLENGE_TITLES = [
|
||||
# Cloudflare
|
||||
"Just a moment...",
|
||||
"Verifying you are human",
|
||||
# DDoS-GUARD
|
||||
"DDoS-Guard",
|
||||
title for titles in CHALLENGE_TITLES_MAP.values() for title in titles
|
||||
]
|
||||
|
||||
+40
-44
@@ -1,27 +1,29 @@
|
||||
import time
|
||||
import warnings
|
||||
from asyncio import wait_for
|
||||
from http import HTTPStatus
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.responses import RedirectResponse
|
||||
from sbase import BaseCase
|
||||
from httpx import TimeoutException
|
||||
from playwright_captcha import CaptchaType
|
||||
|
||||
from src.consts import CAPTCHA_RETRIES, CHALLENGE_TITLES
|
||||
from src.consts import CHALLENGE_TITLES
|
||||
from src.models import (
|
||||
HealthcheckResponse,
|
||||
LinkRequest,
|
||||
LinkResponse,
|
||||
Solution,
|
||||
)
|
||||
from src.utils import get_sb, logger, save_screenshot
|
||||
from src.utils import CamoufoxDepType, get_camoufox, logger
|
||||
|
||||
warnings.filterwarnings("ignore", category=SyntaxWarning)
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
SeleniumDep = Annotated[BaseCase, Depends(get_sb)]
|
||||
CamoufoxDep = Annotated[CamoufoxDepType, Depends(get_camoufox)]
|
||||
|
||||
|
||||
@router.get("/", include_in_schema=False)
|
||||
@@ -32,9 +34,9 @@ def read_root():
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
def health_check(sb: SeleniumDep):
|
||||
async def health_check(sb: CamoufoxDep):
|
||||
"""Health check endpoint."""
|
||||
health_check_request = read_item(
|
||||
health_check_request = await read_item(
|
||||
LinkRequest.model_construct(url="https://google.com"),
|
||||
sb,
|
||||
)
|
||||
@@ -45,58 +47,52 @@ def health_check(sb: SeleniumDep):
|
||||
detail="Health check failed",
|
||||
)
|
||||
|
||||
return HealthcheckResponse(user_agent=sb.get_user_agent())
|
||||
return HealthcheckResponse(user_agent=health_check_request.solution.user_agent)
|
||||
|
||||
|
||||
@router.post("/v1")
|
||||
def read_item(request: LinkRequest, sb: SeleniumDep) -> LinkResponse:
|
||||
async def read_item(request: LinkRequest, dep: CamoufoxDep) -> LinkResponse:
|
||||
"""Handle POST requests."""
|
||||
start_time = int(time.time() * 1000)
|
||||
|
||||
request.url = request.url.replace('"', "").strip()
|
||||
sb.activate_cdp_mode(request.url)
|
||||
sb.sleep(1)
|
||||
page_request = await dep.page.goto(request.url)
|
||||
await dep.page.wait_for_load_state(state="domcontentloaded")
|
||||
await dep.page.wait_for_load_state("networkidle")
|
||||
|
||||
source_bs = sb.get_beautiful_soup()
|
||||
title_tag = source_bs.title
|
||||
if await dep.page.title() in CHALLENGE_TITLES:
|
||||
logger.info("Challenge detected, attempting to solve...")
|
||||
# Solve the captcha
|
||||
remaining_timeout = (
|
||||
request.max_timeout - (int(time.time()) * 1000 - start_time) / 1000
|
||||
)
|
||||
logger.debug(
|
||||
"Remaining timeout for solving the challenge: %d ms", remaining_timeout
|
||||
)
|
||||
try:
|
||||
await wait_for(
|
||||
dep.solver.solve_captcha( # pyright: ignore[reportUnknownMemberType,reportUnknownArgumentType]
|
||||
captcha_container=dep.page,
|
||||
captcha_type=CaptchaType.CLOUDFLARE_INTERSTITIAL,
|
||||
),
|
||||
timeout=remaining_timeout,
|
||||
)
|
||||
except TimeoutException as e:
|
||||
logger.error("Failed to solve challenge: %s", e)
|
||||
return LinkResponse.invalid(url=request.url)
|
||||
logger.debug("Challenge solved successfully.")
|
||||
|
||||
if title_tag and title_tag.string in CHALLENGE_TITLES:
|
||||
for attempt in range(CAPTCHA_RETRIES):
|
||||
try:
|
||||
sb.uc_gui_click_captcha()
|
||||
sb.sleep(2)
|
||||
|
||||
if sb.get_title() not in CHALLENGE_TITLES:
|
||||
break
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.warning(f"Captcha click attempt {attempt + 1} failed: {e}")
|
||||
|
||||
time.sleep(5)
|
||||
|
||||
if sb.get_title() in CHALLENGE_TITLES:
|
||||
save_screenshot(sb)
|
||||
|
||||
raise HTTPException(status_code=500, detail="Could not bypass challenge")
|
||||
|
||||
cookies = sb.get_cookies()
|
||||
for cookie in cookies:
|
||||
name = cookie["name"]
|
||||
value = cookie["value"]
|
||||
cookie["size"] = len(f"{name}={value}".encode())
|
||||
|
||||
cookie["session"] = False
|
||||
if "expiry" in cookie:
|
||||
cookie["expires"] = cookie["expiry"]
|
||||
cookies = await dep.context.cookies()
|
||||
|
||||
return LinkResponse(
|
||||
message="Success",
|
||||
solution=Solution(
|
||||
user_agent=sb.get_user_agent(),
|
||||
url=sb.get_current_url(),
|
||||
status=200,
|
||||
user_agent=await dep.page.evaluate("navigator.userAgent"),
|
||||
url=dep.page.url,
|
||||
status=page_request.status if page_request else HTTPStatus.OK,
|
||||
cookies=cookies,
|
||||
headers={},
|
||||
response=str(sb.get_beautiful_soup()),
|
||||
headers=page_request.headers if page_request else {},
|
||||
response=await dep.page.content(),
|
||||
),
|
||||
start_timestamp=start_time,
|
||||
)
|
||||
|
||||
+3
-2
@@ -1,14 +1,15 @@
|
||||
import time
|
||||
from http import HTTPStatus
|
||||
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from fastapi import Request
|
||||
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
||||
|
||||
from src.models import LinkRequest
|
||||
from src.utils import logger
|
||||
|
||||
|
||||
class LogRequest(BaseHTTPMiddleware):
|
||||
async def dispatch(self, request, call_next):
|
||||
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
|
||||
"""Log requests."""
|
||||
if request.url.path != "/v1" or request.method != "POST":
|
||||
return await call_next(request)
|
||||
|
||||
+6
-2
@@ -4,6 +4,7 @@ import time
|
||||
from http.client import INTERNAL_SERVER_ERROR
|
||||
from typing import Any
|
||||
|
||||
from playwright.sync_api import Cookie
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic.alias_generators import to_camel
|
||||
|
||||
@@ -16,7 +17,10 @@ class LinkRequest(BaseModel):
|
||||
description="Type of request, currently only supports GET requests. This string is purely for compatibility with FlareSolverr.",
|
||||
)
|
||||
url: str = Field(pattern=r"^https?://", default="https://")
|
||||
max_timeout: int = Field(default=60)
|
||||
max_timeout: int = Field(
|
||||
default=60,
|
||||
description="Maximum timeout in seconds for resolving the anti-bot challenge.",
|
||||
)
|
||||
|
||||
|
||||
class HealthcheckResponse(BaseModel):
|
||||
@@ -30,7 +34,7 @@ class Solution(BaseModel):
|
||||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
url: str
|
||||
status: int
|
||||
cookies: list = []
|
||||
cookies: list[Cookie] = []
|
||||
user_agent: str = ""
|
||||
headers: dict[str, Any] = {}
|
||||
response: str = ""
|
||||
|
||||
+58
-41
@@ -1,12 +1,26 @@
|
||||
import logging
|
||||
from time import gmtime, strftime
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import NamedTuple, cast
|
||||
|
||||
from anyio.streams import file
|
||||
from fastapi import Header, HTTPException
|
||||
from httpx import codes
|
||||
from sbase import SB, BaseCase
|
||||
from camoufox import AsyncCamoufox
|
||||
from playwright.async_api import Browser, BrowserContext, Page
|
||||
from playwright_captcha import (
|
||||
ClickSolver,
|
||||
FrameworkType,
|
||||
)
|
||||
|
||||
from src.consts import LOG_LEVEL, PROXY, USE_HEADLESS, USE_XVFB
|
||||
from src.consts import (
|
||||
ADDON_PATH,
|
||||
LOG_LEVEL,
|
||||
MAX_ATTEMPTS,
|
||||
PROXY_PASSWORD,
|
||||
PROXY_SERVER,
|
||||
PROXY_USERNAME,
|
||||
)
|
||||
|
||||
solver_logger = logging.getLogger("playwright_captcha.solvers")
|
||||
solver_logger.handlers.clear()
|
||||
solver_logger.handlers.append(logging.NullHandler())
|
||||
|
||||
logger = logging.getLogger("uvicorn.error")
|
||||
logger.setLevel(LOG_LEVEL)
|
||||
@@ -14,41 +28,44 @@ if len(logger.handlers) == 0:
|
||||
logger.addHandler(logging.StreamHandler())
|
||||
|
||||
|
||||
def get_sb(
|
||||
proxy: str | None = Header(
|
||||
default=PROXY,
|
||||
examples=["protocol://username:password@host:port"],
|
||||
description="Override default proxy address",
|
||||
),
|
||||
):
|
||||
"""Get SeleniumBase instance."""
|
||||
if proxy and proxy.startswith("socks5://") and "@" in proxy:
|
||||
raise HTTPException(
|
||||
status_code=codes.BAD_REQUEST,
|
||||
detail="SOCKS5 proxy with authentication is not supported. Check README for more info.",
|
||||
)
|
||||
|
||||
sb = None
|
||||
try:
|
||||
with SB(
|
||||
uc=True,
|
||||
test=True,
|
||||
headless=USE_HEADLESS,
|
||||
xvfb=USE_XVFB,
|
||||
locale_code="en",
|
||||
ad_block=True,
|
||||
proxy=proxy,
|
||||
) as sb:
|
||||
yield sb
|
||||
except Exception:
|
||||
# Log the exception but re-raise it to let FastAPI handle it properly
|
||||
logger.exception("Exception in SeleniumBase dependency")
|
||||
raise
|
||||
class CamoufoxDepType(NamedTuple):
|
||||
page: Page
|
||||
solver: ClickSolver
|
||||
context: BrowserContext
|
||||
|
||||
|
||||
def save_screenshot(sb: BaseCase):
|
||||
"""Save screenshot on HTTPException."""
|
||||
file_name = f"screenshots_{strftime('%Y-%m-%d_%H:%M:%S', gmtime())}.png"
|
||||
async def get_camoufox() -> AsyncGenerator[CamoufoxDepType, None]:
|
||||
"""Get Camoufox instance."""
|
||||
proxy_config = (
|
||||
{
|
||||
"server": PROXY_SERVER,
|
||||
"username": PROXY_USERNAME,
|
||||
"password": PROXY_PASSWORD,
|
||||
}
|
||||
if PROXY_SERVER
|
||||
else None
|
||||
)
|
||||
|
||||
logger.info(f"Saving screenshot to {file_name}")
|
||||
sb.save_screenshot(file_name)
|
||||
async with AsyncCamoufox(
|
||||
main_world_eval=True,
|
||||
addons=[ADDON_PATH],
|
||||
geoip=True,
|
||||
proxy=proxy_config,
|
||||
locale="en-US",
|
||||
headless=True,
|
||||
humanize=True,
|
||||
i_know_what_im_doing=True,
|
||||
config={"forceScopeAccess": True}, # add this when creating Camoufox instance
|
||||
disable_coop=True, # add this when creating Camoufox instance
|
||||
) as browser_raw:
|
||||
# Cast to Browser since AsyncCamoufox always returns a Browser, not BrowserContext
|
||||
browser = cast("Browser", browser_raw)
|
||||
context = await browser.new_context()
|
||||
page = await context.new_page()
|
||||
async with ClickSolver(
|
||||
framework=FrameworkType.CAMOUFOX,
|
||||
page=page,
|
||||
max_attempts=MAX_ATTEMPTS,
|
||||
attempt_delay=1,
|
||||
) as solver:
|
||||
yield CamoufoxDepType(page, solver, context)
|
||||
|
||||
+11
-10
@@ -1,4 +1,5 @@
|
||||
from http import HTTPStatus
|
||||
from json import JSONDecodeError
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
@@ -13,10 +14,9 @@ test_websites = [
|
||||
"https://ext.to/",
|
||||
# "https://www.ygg.re/",
|
||||
"https://extratorrent.st/",
|
||||
"https://idope.se/",
|
||||
"https://speed.cd/login",
|
||||
'https://www.yggtorrent.top/engine/search?do=search&order=desc&sort=publish_date&name="UNESCAPED"+"DOUBLEQUOTES"&category=2145',
|
||||
"https://freedium.cfd/https://codingplainenglish.medium.com/docker-is-dead-and-its-about-time-b457d14b0a72"
|
||||
"https://1337x.to/home/",
|
||||
]
|
||||
|
||||
|
||||
@@ -31,19 +31,20 @@ def test_bypass(website: str):
|
||||
website,
|
||||
)
|
||||
if (
|
||||
test_request.status_code != HTTPStatus.OK
|
||||
test_request.status_code >= HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
and "Just a moment..." not in test_request.text
|
||||
):
|
||||
pytest.skip(f"Skipping {website} due to {test_request.status_code}")
|
||||
try:
|
||||
error_details = test_request.json()
|
||||
except JSONDecodeError:
|
||||
error_details = test_request.text
|
||||
pytest.skip(
|
||||
f"Skipping {website} - ({test_request.status_code}) {error_details}"
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
"/v1",
|
||||
json={
|
||||
**LinkRequest.model_construct(
|
||||
url=website, max_timeout=30, cmd="request.get"
|
||||
).model_dump(),
|
||||
# "proxy": "203.174.15.83:8080",
|
||||
},
|
||||
json=LinkRequest.model_construct(url=website, cmd="request.get").model_dump(),
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
|
||||
Reference in New Issue
Block a user