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:
Jakub Orchowski
2025-09-05 23:08:35 +02:00
committed by GitHub
parent e745e118b9
commit 7e2d97e1c3
17 changed files with 934 additions and 733 deletions
+7 -1
View File
@@ -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
}
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ on:
schedule:
- cron: "25 0 * * *"
push:
branches: ["main"]
branches: ["*"]
# Publish semver tags as releases.
tags: ["v*.*.*"]
pull_request:
+4 -1
View File
@@ -168,4 +168,7 @@ core
screenshots/
# Downloaded files
downloaded_files/
downloaded_files/
# Browser Data
browser_data/
+2
View File
@@ -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",
+17 -22
View File
@@ -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
View File
@@ -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"]
+9 -11
View File
@@ -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 users 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
View File
@@ -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"
+20 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
Generated
+715 -519
View File
File diff suppressed because it is too large Load Diff