mirror of
https://github.com/ThePhaseless/Byparr.git
synced 2026-09-24 22:03:30 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f98e823991 | ||
|
|
ff49ccb08b | ||
|
|
7cae16ff63 | ||
|
|
34e74b1f7b | ||
|
|
d4c058e7dc | ||
|
|
efcd8f0798 | ||
|
|
4ab174243c | ||
|
|
d4e314e6f9 | ||
|
|
b29e4c826e | ||
|
|
4ab01abbf0 | ||
|
|
834cb76241 | ||
|
|
858bba598f | ||
|
|
27beceaf4d | ||
|
|
eb07de7421 | ||
|
|
7348c0f19b | ||
|
|
6e1c073a6f | ||
|
|
1e443c2dbb | ||
|
|
b153d4ae34 | ||
|
|
a3fec4c584 | ||
|
|
bf8e8ae20f | ||
|
|
ddd325ae29 | ||
|
|
b1704589ae | ||
|
|
6fa5fceff1 | ||
|
|
9ef7a3d3ae | ||
|
|
9eac1f872d | ||
|
|
d5c250c647 | ||
|
|
e150b1a122 | ||
|
|
117c9bdd95 | ||
|
|
a3cd9815bd | ||
|
|
73752f89c8 | ||
|
|
c1dfec8c4d | ||
|
|
64cefb7396 | ||
|
|
ab887df97f | ||
|
|
c5fbc9c363 | ||
|
|
8178f646c0 | ||
|
|
707a9c7a83 | ||
|
|
183f4f7f0f | ||
|
|
23d606a701 | ||
|
|
6b0da3cc5e | ||
|
|
39f758859d | ||
|
|
6fa0658f4c | ||
|
|
d13ab99344 | ||
|
|
c9bcdd6099 | ||
|
|
2263886efe | ||
|
|
5d3ddd9486 | ||
|
|
a2daf5a1e0 | ||
|
|
96d1bdd30f | ||
|
|
d275c56a8d | ||
|
|
09a23ddeb3 | ||
|
|
3c99483a49 | ||
|
|
2223bf722d | ||
|
|
066a6d5f09 | ||
|
|
128a0301f6 | ||
|
|
9ead42863d | ||
|
|
89e9a176c7 | ||
|
|
d5c163f621 | ||
|
|
6694d45e74 | ||
|
|
b23b814e43 | ||
|
|
9353daaf1a | ||
|
|
5a35117299 | ||
|
|
ee8aeed9b1 | ||
|
|
c188d4fe85 | ||
|
|
4ecfc98c81 | ||
|
|
38f344a71c | ||
|
|
815faddcf2 | ||
|
|
999ca88a19 | ||
|
|
dd35795ffa | ||
|
|
147902be2c | ||
|
|
7e9c2ba97d | ||
|
|
9fc9af3ba9 | ||
|
|
55526308f6 | ||
|
|
cdb3223a87 | ||
|
|
329235ba5e | ||
|
|
5031d1e554 | ||
|
|
090b5487e4 | ||
|
|
c608bbc46e | ||
|
|
46eff073b7 | ||
|
|
a347b7046b | ||
|
|
4658d04d2e | ||
|
|
b6aa1a8c58 | ||
|
|
5bff9e1bc1 | ||
|
|
dbd16882de | ||
|
|
adc98e8cea | ||
|
|
8e7d007c6f | ||
|
|
51c1577d7c | ||
|
|
98f9480157 | ||
|
|
2be9fa55a9 | ||
|
|
6520444d4e | ||
|
|
693d67bcfd | ||
|
|
b217778a79 | ||
|
|
3b18dfe522 | ||
|
|
ee5b7fe027 | ||
|
|
d2678643c0 | ||
|
|
343fdca493 | ||
|
|
5a8edf13ab | ||
|
|
1fa6b66f3a | ||
|
|
fe797ba2ac | ||
|
|
409c0f9d8c | ||
|
|
d22e780219 | ||
|
|
d4f722d121 | ||
|
|
38b3407b15 | ||
|
|
81959df037 | ||
|
|
05f9f44c79 | ||
|
|
08cfd95d6a | ||
|
|
f60ecbf2ee | ||
|
|
630389aebe | ||
|
|
603cd0f9a7 |
@@ -1,4 +0,0 @@
|
||||
FROM ghcr.io/astral-sh/uv:debian
|
||||
ENV UV_LINK_MODE=copy
|
||||
|
||||
RUN apt update && apt upgrade -y && apt install -y chromium chromium-driver xvfb git
|
||||
@@ -3,36 +3,22 @@
|
||||
{
|
||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile"
|
||||
"dockerfile": "../Dockerfile",
|
||||
"target": "devcontainer"
|
||||
},
|
||||
"runArgs": ["--security-opt", "label=disable", "--privileged"],
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"charliermarsh.ruff",
|
||||
"-ms-python.autopep8",
|
||||
"-ms-python.vscode-pylance"
|
||||
],
|
||||
"extensions": ["charliermarsh.ruff", "ms-python.python"],
|
||||
"settings": {
|
||||
"python.venvPath": "./venv"
|
||||
}
|
||||
}
|
||||
},
|
||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||
// "features": {},
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
// "postCreateCommand": "pip3 install --user -r requirements.txt",
|
||||
// Configure tool-specific properties.
|
||||
// "customizations": {},
|
||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||
// "remoteUser": "root"
|
||||
"name": "Byparr Dev Container",
|
||||
"runArgs": [
|
||||
"-p",
|
||||
"8181:8191"
|
||||
],
|
||||
"postCreateCommand": "uv sync --group test",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers-extra/features/act:1": {},
|
||||
"ghcr.io/devcontainers/features/desktop-lite:1": {}
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {
|
||||
"moby": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-164
@@ -1,164 +0,0 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||
.pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
.extentions/
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
.gitignore
|
||||
@@ -11,12 +11,19 @@ body:
|
||||
- label: Read Loop Warning on Readme
|
||||
- label: Done the troubleshooting from Readme
|
||||
- label: Checked if such issue already exists
|
||||
- label: Checked other websites with Cloudflare Turnstile
|
||||
- label: Checked other websites with Cloudflare Turnstile
|
||||
- label: I am able to access the webpage from my browser
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: "If checked other websites with Cloudflare Turnstile:"
|
||||
options:
|
||||
- label: Other websites do work.
|
||||
- label: Other websites do not work.
|
||||
|
||||
- type: input
|
||||
id: docker-host
|
||||
attributes:
|
||||
label: OS
|
||||
label: OS/Docker Host
|
||||
placeholder: e.g. Docker, WSL2, Ubuntu, Windows
|
||||
|
||||
- type: input
|
||||
@@ -34,7 +41,7 @@ body:
|
||||
- type: input
|
||||
id: website
|
||||
attributes:
|
||||
label: Website
|
||||
label: Example website/URL that does not work
|
||||
placeholder: e.g. https://nowsecure.nl
|
||||
|
||||
- type: textarea
|
||||
|
||||
@@ -9,7 +9,7 @@ on:
|
||||
schedule:
|
||||
- cron: "25 0 * * *"
|
||||
push:
|
||||
branches: ["main"]
|
||||
branches: ["*"]
|
||||
# Publish semver tags as releases.
|
||||
tags: ["v*.*.*"]
|
||||
pull_request:
|
||||
@@ -22,6 +22,10 @@ env:
|
||||
# github.repository as <account>/<repo>
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -32,7 +36,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
@@ -62,7 +66,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Prepare variables
|
||||
id: vars
|
||||
@@ -108,7 +112,9 @@ jobs:
|
||||
platforms: ${{ matrix.platform }}
|
||||
cache-from: type=gha,scope=${{ matrix.platform }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.platform }}
|
||||
build-args: GITHUB_BUILD=true,VERSION=${{ github.ref_type == 'tag' && github.ref_name || github.sha }}
|
||||
build-args: |
|
||||
GITHUB_BUILD=true
|
||||
VERSION=${{ github.ref_type == 'tag' && github.ref_name || github.sha }}
|
||||
|
||||
merge-and-push:
|
||||
needs: build
|
||||
@@ -121,7 +127,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
# Install the cosign tool
|
||||
- name: Install cosign
|
||||
|
||||
+4
-1
@@ -168,4 +168,7 @@ core
|
||||
screenshots/
|
||||
|
||||
# Downloaded files
|
||||
downloaded_files/
|
||||
downloaded_files/
|
||||
|
||||
# Browser Data
|
||||
browser_data/
|
||||
|
||||
Vendored
+16
-18
@@ -1,19 +1,17 @@
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"console": "integratedTerminal",
|
||||
"env": {
|
||||
"LOG_LEVEL": "DEBUG"
|
||||
},
|
||||
"justMyCode": false,
|
||||
"name": "Python Debugger: Main",
|
||||
"program": "main.py",
|
||||
"request": "launch",
|
||||
"type": "debugpy"
|
||||
},
|
||||
],
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0"
|
||||
}
|
||||
"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",
|
||||
"request": "launch",
|
||||
"type": "debugpy"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
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"]
|
||||
}
|
||||
|
||||
+19
-17
@@ -1,40 +1,42 @@
|
||||
FROM debian:bullseye-slim AS base
|
||||
FROM debian:stable-slim AS base
|
||||
ENV HOME=/root
|
||||
|
||||
ARG GITHUB_BUILD=false \
|
||||
VERSION
|
||||
|
||||
ENV HOME=/root \
|
||||
GITHUB_BUILD=${GITHUB_BUILD}\
|
||||
ENV GITHUB_BUILD=${GITHUB_BUILD}\
|
||||
VERSION=${VERSION}\
|
||||
DEBIAN_FRONTEND=noninteractive \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
# prevents python creating .pyc files
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
DISPLAY=:0
|
||||
ENV PATH="${HOME}/.local/bin:$PATH"
|
||||
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
|
||||
RUN apt update && apt -y upgrade && apt install -y curl
|
||||
|
||||
ADD https://astral.sh/uv/install.sh install.sh
|
||||
RUN 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
|
||||
ENTRYPOINT [ "sleep", "infinity" ]
|
||||
|
||||
|
||||
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 && ln -s /usr/bin/chromedriver uc_driver
|
||||
|
||||
COPY . .
|
||||
|
||||
FROM base AS test
|
||||
|
||||
FROM app AS test
|
||||
RUN --mount=type=cache,target=${HOME}/.cache/uv uv sync --group test
|
||||
RUN ./test.sh
|
||||
|
||||
FROM base
|
||||
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" ]
|
||||
CMD ["./cmd.sh"]
|
||||
ENTRYPOINT ["uv", "run", "main.py"]
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# Byparr
|
||||
|
||||
An alternative to [FlareSolverr](https://github.com/FlareSolverr/FlareSolverr) as a drop-in replacement, built with [seleniumbase](https://seleniumbase.io/) and [FastAPI](https://fastapi.tiangolo.com).
|
||||
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]
|
||||
> Due to recent challenge changes, this software does not guarantee that the Cloudflare challenge will be bypassed. Cloudflare likely requires valid network traffic originating from the user’s public IP address to mark a connection as legitimate. While this tool may bypass the initial browser check, it does not ensure that requests will consistently pass Cloudflare's validation. More testing and data are required to understand how Cloudflare identifies connections and requests as valid. Invalid requests will result in Byparr's looping and eventually time-outing.
|
||||
> 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.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Support for NAS devices (like Synology) is minimal. Please report issues, but do not expect it to be fixed quickly. The only ARM device I have is a free Ampere Oracle VM, so I can only test ARM support on that. See [#22](https://github.com/ThePhaseless/Byparr/issues/22) and [#3](https://github.com/ThePhaseless/Byparr/issues/3)
|
||||
@@ -33,11 +33,16 @@ An alternative to [FlareSolverr](https://github.com/FlareSolverr/FlareSolverr) a
|
||||
|
||||
## Options
|
||||
|
||||
| Env | Default | Description |
|
||||
| -------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `USE_XVFB` | `SeleniumBase default` | Use virtual desktop with Xvfb. (Linux only) (Can cause performance hog [#14](https://github.com/ThePhaseless/Byparr/issues/14)) |
|
||||
| `USE_HEADLESS` | `SeleniumBase default` | Use headless chromium. |
|
||||
| `PROXY` | None | Proxy to use. (format: `username:password@host:port`) |
|
||||
| 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** _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
|
||||
|
||||
@@ -59,12 +64,12 @@ docker run -p 8191:8191 ghcr.io/thephaseless/byparr:latest
|
||||
### Local
|
||||
|
||||
```bash
|
||||
uv sync && ./cmd.sh
|
||||
uv sync && uv run main.py
|
||||
```
|
||||
|
||||
## Need help with / TODO
|
||||
|
||||
- [x] Slimming container (only ~650 MB now!)
|
||||
- [x] Slimming container (only ~1.11 GB now!)
|
||||
- [x] Add more anti-bot challenges
|
||||
- [x] Add doc strings
|
||||
- [x] Implement versioning
|
||||
|
||||
+5
-7
@@ -1,12 +1,10 @@
|
||||
services:
|
||||
byparr:
|
||||
image: ghcr.io/thephaseless/byparr
|
||||
image: ghcr.io/thephaseless/byparr:latest
|
||||
restart: unless-stopped
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
environment:
|
||||
- LOG_LEVEL=INFO
|
||||
volumes:
|
||||
- ./screenshots:/app/screenshots # For screenshots when exception occurs
|
||||
ports:
|
||||
- "8191:8191"
|
||||
# Uncomment below to use byparr outside of internal network
|
||||
# ports:
|
||||
# - "8191:8191"
|
||||
|
||||
@@ -1,15 +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)
|
||||
|
||||
@@ -20,5 +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__":
|
||||
uvicorn.run(app, host="0.0.0.0", 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)
|
||||
|
||||
+15
-16
@@ -8,23 +8,24 @@ version = "0.1.0"
|
||||
description = "API for getting cookies for Cloudflare challenges"
|
||||
readme = "README.md"
|
||||
dependencies = [
|
||||
"fastapi[standard]==0.115.12",
|
||||
"pyautogui==0.9.54",
|
||||
"pydantic==2.11.2",
|
||||
"seleniumbase==4.37.1",
|
||||
"uvicorn==0.34.0",
|
||||
"camoufox[geoip]==0.4.*",
|
||||
"fastapi[standard]==0.116.*",
|
||||
"playwright-captcha==0.1.*",
|
||||
"pydantic==2.11.*",
|
||||
]
|
||||
urls = { repository = "https://github.com/ThePhaseless/Byparr" }
|
||||
|
||||
[dependency-groups]
|
||||
test = [
|
||||
"httpx==0.28.1",
|
||||
"pytest-asyncio==0.26.0",
|
||||
"pytest-progress==1.3.0",
|
||||
"pytest-retry==1.7.0",
|
||||
"pytest-xdist==3.6.1",
|
||||
"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.0", "ruff==0.11.4"]
|
||||
dev = ["deptry==0.23.*", "ruff==0.12.*"]
|
||||
|
||||
[tool.deptry.per_rule_ignores]
|
||||
DEP002 = ["pyautogui"]
|
||||
@@ -58,8 +59,6 @@ ignore = [
|
||||
select = ["ALL"]
|
||||
extend-safe-fixes = ["D415"]
|
||||
|
||||
[tool.pytest]
|
||||
log_cli = "True"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
asyncio_default_fixture_loop_scope = "function"
|
||||
[tool.pyright]
|
||||
reportMissingTypeStubs = false
|
||||
typeCheckingMode = "strict"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:recommended"],
|
||||
"forkProcessing": "enabled",
|
||||
"packageRules": [
|
||||
{
|
||||
"automerge": true,
|
||||
|
||||
+21
-35
@@ -1,43 +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,
|
||||
)
|
||||
|
||||
PROXY = os.getenv("PROXY")
|
||||
LOG_LEVEL = logging.getLevelNamesMapping()[os.getenv("LOG_LEVEL", "INFO").upper()]
|
||||
|
||||
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...",
|
||||
# DDoS-GUARD
|
||||
"DDoS-Guard",
|
||||
title for titles in CHALLENGE_TITLES_MAP.values() for title in titles
|
||||
]
|
||||
|
||||
Executable → Regular
+44
-31
@@ -1,23 +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 CHALLENGE_TITLES
|
||||
from src.models import (
|
||||
HealthcheckResponse,
|
||||
LinkRequest,
|
||||
LinkResponse,
|
||||
Solution,
|
||||
)
|
||||
from src.utils import CamoufoxDepType, get_camoufox, logger
|
||||
|
||||
warnings.filterwarnings("ignore", category=SyntaxWarning)
|
||||
|
||||
from .utils import get_sb, logger, save_screenshot
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
SeleniumDep = Annotated[BaseCase, Depends(get_sb)]
|
||||
CamoufoxDep = Annotated[CamoufoxDepType, Depends(get_camoufox)]
|
||||
|
||||
|
||||
@router.get("/", include_in_schema=False)
|
||||
@@ -28,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,
|
||||
)
|
||||
@@ -41,45 +47,52 @@ def health_check(sb: SeleniumDep):
|
||||
detail="Health check failed",
|
||||
)
|
||||
|
||||
return {"status": "ok"}
|
||||
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)
|
||||
sb.uc_open_with_reconnect(request.url)
|
||||
logger.debug(f"Got webpage: {request.url}")
|
||||
source_bs = sb.get_beautiful_soup()
|
||||
title_tag = source_bs.title
|
||||
if title_tag and title_tag.string in CHALLENGE_TITLES:
|
||||
logger.debug("Challenge detected")
|
||||
sb.uc_gui_click_captcha()
|
||||
logger.info("Clicked captcha")
|
||||
|
||||
if sb.get_title() in CHALLENGE_TITLES:
|
||||
save_screenshot(sb)
|
||||
raise HTTPException(status_code=500, detail="Could not bypass challenge")
|
||||
request.url = request.url.replace('"', "").strip()
|
||||
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")
|
||||
|
||||
cookies = sb.get_cookies()
|
||||
for cookie in cookies:
|
||||
name = cookie["name"]
|
||||
value = cookie["value"]
|
||||
cookie["size"] = len(f"{name}={value}".encode())
|
||||
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.")
|
||||
|
||||
cookie["session"] = False
|
||||
if "expiry" in cookie:
|
||||
cookie["expires"] = cookie["expiry"]
|
||||
cookies = await dep.context.cookies()
|
||||
|
||||
return LinkResponse(
|
||||
message="Success",
|
||||
solution=Solution(
|
||||
userAgent=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,
|
||||
)
|
||||
|
||||
+4
-3
@@ -1,16 +1,17 @@
|
||||
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":
|
||||
if request.url.path != "/v1" or request.method != "POST":
|
||||
return await call_next(request)
|
||||
|
||||
start_time = time.perf_counter()
|
||||
|
||||
+19
-23
@@ -1,9 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from http import HTTPStatus
|
||||
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,32 +17,27 @@ 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):
|
||||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
msg: str = "Byparr is ready!"
|
||||
version: str = consts.VERSION
|
||||
user_agent: str
|
||||
|
||||
|
||||
class Solution(BaseModel):
|
||||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
url: str
|
||||
status: int
|
||||
cookies: list
|
||||
userAgent: str # noqa: N815 # Ignore to preserve compatibility
|
||||
headers: dict[str, Any]
|
||||
response: str
|
||||
|
||||
@classmethod
|
||||
def invalid(cls, url: str):
|
||||
"""
|
||||
Return an empty Solution with default values.
|
||||
|
||||
Useful for returning an error response.
|
||||
"""
|
||||
return cls(
|
||||
url=url,
|
||||
status=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
cookies=[],
|
||||
userAgent="",
|
||||
headers={},
|
||||
response="",
|
||||
)
|
||||
cookies: list[Cookie] = []
|
||||
user_agent: str = ""
|
||||
headers: dict[str, Any] = {}
|
||||
response: str = ""
|
||||
|
||||
|
||||
class LinkResponse(BaseModel):
|
||||
@@ -63,6 +59,6 @@ class LinkResponse(BaseModel):
|
||||
return cls(
|
||||
status="error",
|
||||
message="Invalid request",
|
||||
solution=Solution.invalid(url),
|
||||
solution=Solution(url=url, status=INTERNAL_SERVER_ERROR),
|
||||
start_timestamp=int(time.time() * 1000),
|
||||
)
|
||||
|
||||
+59
-23
@@ -1,10 +1,26 @@
|
||||
import logging
|
||||
from time import gmtime, strftime
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import NamedTuple, cast
|
||||
|
||||
from fastapi import Header
|
||||
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
|
||||
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)
|
||||
@@ -12,24 +28,44 @@ if len(logger.handlers) == 0:
|
||||
logger.addHandler(logging.StreamHandler())
|
||||
|
||||
|
||||
def get_sb(
|
||||
proxy: str | None = Header(
|
||||
default=None,
|
||||
examples=["username:password@host:port"],
|
||||
description="Override default proxy from env",
|
||||
),
|
||||
):
|
||||
"""Get SeleniumBase instance."""
|
||||
with SB(
|
||||
uc=True,
|
||||
headless=USE_HEADLESS,
|
||||
locale_code="en",
|
||||
ad_block=True,
|
||||
proxy=proxy or PROXY,
|
||||
) as sb:
|
||||
yield sb
|
||||
class CamoufoxDepType(NamedTuple):
|
||||
page: Page
|
||||
solver: ClickSolver
|
||||
context: BrowserContext
|
||||
|
||||
|
||||
def save_screenshot(sb: BaseCase):
|
||||
"""Save screenshot on HTTPException."""
|
||||
sb.save_screenshot(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
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
+13
-10
@@ -1,4 +1,5 @@
|
||||
from http import HTTPStatus
|
||||
from json import JSONDecodeError
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
@@ -11,10 +12,11 @@ client = TestClient(app)
|
||||
|
||||
test_websites = [
|
||||
"https://ext.to/",
|
||||
"https://www.ygg.re/",
|
||||
# "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://1337x.to/home/",
|
||||
]
|
||||
|
||||
|
||||
@@ -29,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