mirror of
https://github.com/ThePhaseless/Byparr.git
synced 2026-09-24 22:03:30 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
332402652d | ||
|
|
061f0d44de | ||
|
|
b7914268bc | ||
|
|
2e8ed3fa9b | ||
|
|
6f6941e423 | ||
|
|
25ae01ba71 | ||
|
|
a065b4b7ae | ||
|
|
508568ded7 | ||
|
|
72f54fc0a9 | ||
|
|
1499cfa3df | ||
|
|
cf46f481e1 | ||
|
|
c90b0ab91e | ||
|
|
f5e75b451c | ||
|
|
0fbae3f9dd | ||
|
|
70f99e40fc | ||
|
|
6a56098a33 | ||
|
|
7dd1dd633f | ||
|
|
e825e588a8 | ||
|
|
43fe522417 | ||
|
|
ad88b7b505 | ||
|
|
78deb9ed34 | ||
|
|
65f39b4448 | ||
|
|
565b076c67 | ||
|
|
0ea76ac000 | ||
|
|
52f4b6110a | ||
|
|
8c9795150e | ||
|
|
44c3d5b512 | ||
|
|
817610d216 | ||
|
|
bf1daabaf9 | ||
|
|
9445ffc5f6 | ||
|
|
67303951bb | ||
|
|
42303d5582 | ||
|
|
35c6189f6d | ||
|
|
1e6bd9e656 | ||
|
|
7bd7925530 | ||
|
|
f0dd9517e7 | ||
|
|
af72a32870 | ||
|
|
d0dea96f63 | ||
|
|
d7138776a5 | ||
|
|
7e2c895a11 | ||
|
|
29793054f8 | ||
|
|
9fbf4982c8 | ||
|
|
f60d828914 | ||
|
|
bdcf756b7e | ||
|
|
61ef009944 | ||
|
|
02fdfb619a | ||
|
|
978efa7a5b | ||
|
|
a912a32eea | ||
|
|
b8a3995c8c | ||
|
|
59104a3c6e | ||
|
|
a9edd671a2 | ||
|
|
5c5451785e | ||
|
|
4d11b950a1 | ||
|
|
df161e6d2e | ||
|
|
8e53210014 | ||
|
|
43cd9297cb | ||
|
|
19670c8e06 | ||
|
|
eba16fc3de | ||
|
|
ecbb2ab182 | ||
|
|
36421b2c1b | ||
|
|
ac953fe704 | ||
|
|
30f25acd55 | ||
|
|
554347ce6f | ||
|
|
58a486c41d | ||
|
|
dc80b0cccf | ||
|
|
84a70ad35b | ||
|
|
6618bcf651 | ||
|
|
620c768375 | ||
|
|
9244768ce4 | ||
|
|
9bd13e19a0 | ||
|
|
ecd9e0aee2 | ||
|
|
78137997e1 | ||
|
|
03c98375f2 | ||
|
|
4bfb7845dd | ||
|
|
d7c22f042e | ||
|
|
33a1327f93 | ||
|
|
e52616934b | ||
|
|
892daccc60 | ||
|
|
3d91874ddb | ||
|
|
3026ed4a41 | ||
|
|
4b5d82b4f1 | ||
|
|
56d9a4659b | ||
|
|
273622c30c | ||
|
|
d0b51020a0 | ||
|
|
d17def34ba | ||
|
|
40a4bf3d8f | ||
|
|
e0bb4115fb | ||
|
|
9e2d40845c | ||
|
|
6c3da87c00 | ||
|
|
78d97dfd0d | ||
|
|
4461f5a59a | ||
|
|
7dac738ac5 | ||
|
|
ead7a09ce2 | ||
|
|
bf85a854fa | ||
|
|
9a20451ad9 | ||
|
|
150bf80583 | ||
|
|
bcce6779cc | ||
|
|
cd7e803411 | ||
|
|
5dc76167c2 | ||
|
|
8ae150fd87 | ||
|
|
0325877e19 | ||
|
|
c4159e5c38 | ||
|
|
83169ce019 | ||
|
|
1c0929fbc5 | ||
|
|
9170967455 | ||
|
|
d5d0bf7923 | ||
|
|
17854c70ed | ||
|
|
e30954c98c | ||
|
|
2556a1bc88 | ||
|
|
6c065d4dd3 | ||
|
|
096eda0fa3 | ||
|
|
2ebb1bd820 | ||
|
|
bf37539890 | ||
|
|
d7ef83b8c6 | ||
|
|
1a7cd9d657 | ||
|
|
f9387e6942 | ||
|
|
9d8c8db979 | ||
|
|
979e0b4803 | ||
|
|
3dbe3c424c | ||
|
|
bbb7c73065 | ||
|
|
9a353b3da2 | ||
|
|
140a97003e | ||
|
|
c9e29f578d | ||
|
|
c37ab51179 | ||
|
|
962db9409c | ||
|
|
96a9cc7ba9 | ||
|
|
bf9460b960 | ||
|
|
c0547192dd | ||
|
|
008077076c | ||
|
|
b33444dc48 | ||
|
|
b8c8576e5a | ||
|
|
905d802a26 | ||
|
|
f99429fdea | ||
|
|
1a4ec03587 | ||
|
|
ee2630dce2 | ||
|
|
82130b77c0 | ||
|
|
faf7e5e51a | ||
|
|
597faa48dc | ||
|
|
a28dbbe972 | ||
|
|
82124dee9b | ||
|
|
0551c7b82b | ||
|
|
b483dba080 | ||
|
|
7533d039ac | ||
|
|
38b2ea0798 | ||
|
|
58ae7964df | ||
|
|
5283fb2d0e | ||
|
|
b3e7662782 | ||
|
|
074a3dd712 | ||
|
|
cb48f16370 | ||
|
|
637de7572d | ||
|
|
7b55252a73 | ||
|
|
cbe4cf57a5 | ||
|
|
0775d107ad | ||
|
|
d36972b80e | ||
|
|
27ec17f7f2 | ||
|
|
0b2bf9e12a | ||
|
|
8c6906c916 | ||
|
|
3103051ccf |
@@ -1,5 +1,4 @@
|
||||
FROM python:3.12-bullseye
|
||||
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
|
||||
|
||||
RUN curl -sSL https://install.python-poetry.org | python3 -
|
||||
RUN apt update && apt upgrade -y && apt install -y chromium chromium-driver xvfb git
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**I've:**
|
||||
|
||||
- [ ] - read Loop Warning on Readme.
|
||||
- [ ] - done the troubleshooting from Readme.
|
||||
- [ ] - checked if such issue already exists.
|
||||
|
||||
**Docker host (please complete the following information):**
|
||||
|
||||
- OS: [e.g. Ubuntu Linux]
|
||||
- Platform and model: [Synology, PC, Laptop, RPi 3]
|
||||
- Architecture [e.g. ARM64]
|
||||
- Website: [e.g. https://nowsecure.nl]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,47 @@
|
||||
name: Bug report
|
||||
description: Create a bug report
|
||||
assignees: ThePhaseless
|
||||
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: "I've completed the following steps:"
|
||||
options:
|
||||
- label: Read Loop Warning on Readme
|
||||
- label: Done the troubleshooting from Readme
|
||||
- label: Checked if such issue already exists
|
||||
|
||||
- type: input
|
||||
id: docker-host
|
||||
attributes:
|
||||
label: OS
|
||||
placeholder: e.g. Docker, Ubuntu Linux
|
||||
|
||||
- type: input
|
||||
id: platform-model
|
||||
attributes:
|
||||
label: Platform and model
|
||||
placeholder: e.g. Synology, PC, Laptop, RPi 3
|
||||
|
||||
- type: input
|
||||
id: architecture
|
||||
attributes:
|
||||
label: Architecture
|
||||
placeholder: e.g. ARM64
|
||||
|
||||
- type: input
|
||||
id: website
|
||||
attributes:
|
||||
label: Website
|
||||
placeholder: e.g. https://nowsecure.nl
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: A clear and concise description of what the bug is.
|
||||
placeholder: "Example: The application crashes when trying to load a new file"
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context about the problem here.
|
||||
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
@@ -25,45 +25,28 @@ env:
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Test
|
||||
id: test
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: "pip"
|
||||
|
||||
- name: Set up Poetry
|
||||
run: pip install poetry
|
||||
|
||||
- name: Setup a local virtual environment (if no poetry.toml file)
|
||||
run: |
|
||||
poetry config virtualenvs.create true --local
|
||||
poetry config virtualenvs.in-project true --local
|
||||
|
||||
- uses: actions/cache@v4
|
||||
name: Define a cache for the virtual environment based on the dependencies lock file
|
||||
with:
|
||||
path: ./.venv
|
||||
key: venv-${{ hashFiles('poetry.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
poetry install
|
||||
sudo apt update
|
||||
sudo apt install -y xvfb scrot python3-tk
|
||||
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
|
||||
sudo apt install -y ./google-chrome-stable_current_amd64.deb
|
||||
rm ./google-chrome-stable_current_amd64.deb
|
||||
|
||||
- name: Run tests
|
||||
run: poetry run pytest --retries 2 -n auto --xvfb
|
||||
|
||||
- name: Upload screenshots if tests fail
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: screenshots
|
||||
context: .
|
||||
platforms: linux/amd64
|
||||
cache-from: type=gha,scope=x64
|
||||
pull: true
|
||||
cache-to: type=gha,mode=max,scope=x64
|
||||
target: test
|
||||
|
||||
build:
|
||||
needs: test
|
||||
@@ -71,43 +54,95 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
# This is used to complete the identity challenge
|
||||
# with sigstore/fulcio when running outside of PRs.
|
||||
id-token: write
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
platform: [linux/amd64, linux/arm64]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Install the cosign tool except on PR
|
||||
# https://github.com/sigstore/cosign-installer
|
||||
- name: Install cosign
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da #v3.7.0
|
||||
with:
|
||||
cosign-release: "v2.2.4"
|
||||
- name: Prepare variables
|
||||
id: vars
|
||||
run: |
|
||||
SURFIX=$(echo ${{ matrix.platform }} | cut -d'/' -f2)
|
||||
echo "SURFIX=$SURFIX" >> $GITHUB_OUTPUT
|
||||
# Generate a unique local tag for the image
|
||||
echo "LOCAL_TAG=${{ github.sha }}-$SURFIX" >> $GITHUB_OUTPUT
|
||||
|
||||
# Set up BuildKit Docker container builder to be able to build
|
||||
# multi-platform images and export cache
|
||||
# https://github.com/docker/setup-buildx-action
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
# Set up BuildKit Docker container builder
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# Login against a Docker registry except on PR
|
||||
# https://github.com/docker/login-action
|
||||
# Log into registry
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Extract metadata (tags, labels) for Docker
|
||||
# https://github.com/docker/metadata-action
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5.6.1
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
tags: type=raw,value=${{ steps.vars.outputs.LOCAL_TAG }}
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
|
||||
# Build and export Docker image for each platform (without pushing)
|
||||
- name: Build Docker image
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
pull: true
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
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 }}
|
||||
|
||||
merge-and-push:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request'
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Install the cosign tool
|
||||
- name: Install cosign
|
||||
uses: sigstore/cosign-installer@v3
|
||||
|
||||
# Set up Docker Buildx
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# Log into registry
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Extract Docker metadata for tagging
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
@@ -115,35 +150,37 @@ jobs:
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=raw,enable=${{ github.event_name != 'pull_request' && github.ref_name == 'main' }}, value=latest
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
|
||||
# Build and push Docker image with Buildx (don't push on PR)
|
||||
# https://github.com/docker/build-push-action
|
||||
- name: Build and push Docker image
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6.10.0
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: GITHUB_BUILD=true
|
||||
# Create manifest lists and push
|
||||
- name: Create and push manifest lists
|
||||
run: |
|
||||
TAGS="${{ steps.meta.outputs.tags }}"
|
||||
args=""
|
||||
|
||||
# Sign the resulting Docker image digest except on PRs.
|
||||
# This will only write to the public Rekor transparency log when the Docker
|
||||
# repository is public to avoid leaking data. If you would like to publish
|
||||
# transparency data even for private images, pass --force to cosign below.
|
||||
# https://github.com/sigstore/cosign
|
||||
- name: Sign the published Docker image
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
image=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
image=${image,,}
|
||||
|
||||
# Create tag arguments
|
||||
for tag in $TAGS; do
|
||||
args="$args -t $tag"
|
||||
done
|
||||
|
||||
if [ "${{ github.ref_type }}" == "tag" ]; then
|
||||
args="$args -t ${image}:latest"
|
||||
fi
|
||||
|
||||
echo $args
|
||||
|
||||
docker buildx imagetools create $args \
|
||||
${image}:${{github.sha}}-amd64 \
|
||||
${image}:${{github.sha}}-arm64
|
||||
|
||||
# Sign the manifest
|
||||
- name: Sign the manifests
|
||||
env:
|
||||
# https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable
|
||||
TAGS: ${{ steps.meta.outputs.tags }}
|
||||
DIGEST: ${{ steps.build-and-push.outputs.digest }}
|
||||
# This step uses the identity token to provision an ephemeral certificate
|
||||
# against the sigstore community Fulcio instance.
|
||||
run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST}
|
||||
run: |
|
||||
for TAG in $TAGS; do
|
||||
cosign sign --yes $TAG
|
||||
done
|
||||
|
||||
+1
-1
@@ -165,7 +165,7 @@ cython_debug/
|
||||
core
|
||||
|
||||
# Screenshots
|
||||
*.png
|
||||
screenshots/
|
||||
|
||||
# Downloaded files
|
||||
downloaded_files/
|
||||
Vendored
+2
-2
@@ -13,10 +13,10 @@
|
||||
"python.analysis.typeCheckingMode": "standard",
|
||||
"python.terminal.activateEnvironment": true,
|
||||
"python.testing.pytestArgs": [
|
||||
"tests",
|
||||
"-n",
|
||||
"auto",
|
||||
"--retries=2",
|
||||
"--xvfb"
|
||||
"--retries=3",
|
||||
],
|
||||
"python.testing.pytestEnabled": true,
|
||||
"python.testing.unittestEnabled": false
|
||||
|
||||
+27
-19
@@ -1,32 +1,40 @@
|
||||
FROM python:3.12-slim-bullseye
|
||||
FROM debian:bullseye-slim AS base
|
||||
|
||||
# Inspired by https://github.com/Hudrolax/uc-docker-alpine/
|
||||
ARG GITHUB_BUILD=false \
|
||||
VERSION
|
||||
|
||||
ARG GITHUB_BUILD=false
|
||||
ENV GITHUB_BUILD=${GITHUB_BUILD}
|
||||
|
||||
ENV HOME=/root
|
||||
ENV \
|
||||
ENV HOME=/root \
|
||||
GITHUB_BUILD=${GITHUB_BUILD}\
|
||||
VERSION=${VERSION}\
|
||||
DEBIAN_FRONTEND=noninteractive \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
# prevents python creating .pyc files
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
# do not ask any interactive question
|
||||
POETRY_NO_INTERACTION=1 \
|
||||
POETRY_VIRTUALENVS_IN_PROJECT=true \
|
||||
DISPLAY=:0
|
||||
ENV PATH="${HOME}/.local/bin:$PATH"
|
||||
|
||||
WORKDIR /app
|
||||
EXPOSE 8191
|
||||
RUN apt update &&\
|
||||
apt install -y xvfb scrot python3-tk curl chromium chromium-driver
|
||||
|
||||
RUN curl -sSL https://install.python-poetry.org | python3 -
|
||||
ENV PATH="${HOME}/.local/bin:$PATH"
|
||||
COPY pyproject.toml poetry.lock ./
|
||||
RUN poetry install
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends --no-install-suggests xauth xvfb scrot curl chromium chromium-driver ca-certificates
|
||||
|
||||
ADD https://astral.sh/uv/install.sh install.sh
|
||||
RUN sh install.sh
|
||||
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 . .
|
||||
HEALTHCHECK --interval=60s --timeout=30s --start-period=5s --retries=3 CMD [ "curl", "http://localhost:8191/health" ]
|
||||
RUN ln /usr/bin/chromedriver ./.venv/lib/python3.12/site-packages/seleniumbase/drivers/uc_driver
|
||||
|
||||
FROM base AS test
|
||||
|
||||
RUN --mount=type=cache,target=${HOME}/.cache/uv uv sync --group test
|
||||
RUN ./test.sh
|
||||
|
||||
FROM base
|
||||
|
||||
EXPOSE 8191
|
||||
HEALTHCHECK --interval=15m --timeout=30s --start-period=5s --retries=3 CMD [ "curl", "http://localhost:8191/health" ]
|
||||
CMD ["./cmd.sh"]
|
||||
@@ -1,11 +1,11 @@
|
||||
# Byparr
|
||||
|
||||
An alternative to [FlareSolverr](https://github.com/FlareSolverr/FlareSolverr) as a drop-in replacement, build with [seleniumbase](https://seleniumbase.io/) and [FastAPI](https://fastapi.tiangolo.com).
|
||||
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).
|
||||
|
||||
> [!WARNING]
|
||||
> [!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.
|
||||
|
||||
> [!WARNING]
|
||||
> [!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)
|
||||
|
||||
> [!NOTE]
|
||||
@@ -13,35 +13,60 @@ An alternative to [FlareSolverr](https://github.com/FlareSolverr/FlareSolverr) a
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
1. Clone repo to the host that has the container has issues on.
|
||||
2. Using vscode and `SSH extention`, connect to the host and open repo in it.
|
||||
3. Download `devcontainers` extention and reopen repo in container (with `CTRL + SHIFT + P` -> `Reopen in devcontainer`)
|
||||
4. Forward port 6080 from devcontainer (port of noVNC server) to the host.
|
||||
5. Open `http://localhost:6080` and connect to the virtual desktop.
|
||||
6. Check if `chromium` works by running in VNC's terminal command `chromium --no-sandbox`.
|
||||
7. If chromium works, run (or debug) tests from VS Code.
|
||||
1. If code works, congrats! (/s) You are on your own.
|
||||
2. If it does not, try another host or network, try again and create issue about the problem.
|
||||
### Docker
|
||||
|
||||
1. Clone repo to the host that has issues with Byparr.
|
||||
2. Run `docker build --target test .`
|
||||
3. Depending of the build success:
|
||||
1. If run successfully, try updating container or if already on newest stable release create an issue for creating new release with new dependencies
|
||||
2. If build fails, try troubleshooting on another host/using other method
|
||||
|
||||
### Local
|
||||
|
||||
1. Download [uv](https://docs.astral.sh/uv/getting-started/installation/)
|
||||
2. Download dependencies using `uv sync --group test`
|
||||
3. Run tests with `uv run pytest --retries 3` (You can add `-n auto` for parallelization)
|
||||
4. If you see any `F` character in terminal, that means test failed even after retries.
|
||||
5. Depending of the test success:
|
||||
1. If run successfully, try updating container or if already on newest stable release create an issue for creating new release with new dependencies
|
||||
2. If test fails, try troubleshooting on another host/using other method
|
||||
|
||||
## 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`) |
|
||||
|
||||
## Tags
|
||||
|
||||
- `v*.*.*`/`latest` - Releases considered stable
|
||||
- `main` - Latest release from main branch (untested)
|
||||
|
||||
## Usage
|
||||
|
||||
### Docker Compose
|
||||
|
||||
```yaml
|
||||
services:
|
||||
byparr:
|
||||
image: ghcr.io/thephaseless/byparr:latest
|
||||
environment:
|
||||
- LOG_LEVEL=INFO # optional
|
||||
ports:
|
||||
- "8191:8191" # Optional if needed to make make requests/check docs on host
|
||||
See `compose.yaml`
|
||||
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
docker run -p 8191:8191 ghcr.io/thephaseless/byparr:latest
|
||||
```
|
||||
|
||||
### Local
|
||||
|
||||
```bash
|
||||
uv sync && ./cmd.sh
|
||||
```
|
||||
|
||||
## Need help with / TODO
|
||||
|
||||
- [x] Slimming container (only ~650 mB now!)
|
||||
- [x] Slimming container (only ~650 MB now!)
|
||||
- [x] Add more anti-bot challenges
|
||||
- [x] Add docstrings
|
||||
- [x] Add doc strings
|
||||
- [x] Implement versioning
|
||||
- [ ] Proxy support
|
||||
- [x] Proxy support
|
||||
- [x] Add more architectures support
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
. .venv/bin/activate && python3 main.py
|
||||
uv run main.py
|
||||
@@ -6,5 +6,7 @@ services:
|
||||
dockerfile: Dockerfile
|
||||
environment:
|
||||
- LOG_LEVEL=INFO
|
||||
volumes:
|
||||
- ./screenshots:/app/screenshots # For screenshots when exception occurs
|
||||
ports:
|
||||
- "8191:8191"
|
||||
@@ -1,110 +1,23 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import time
|
||||
from http import HTTPStatus
|
||||
|
||||
import uvicorn
|
||||
from bs4 import BeautifulSoup
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.responses import RedirectResponse
|
||||
from sbase import SB, BaseCase
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.gzip import GZipMiddleware
|
||||
|
||||
import src
|
||||
import src.utils
|
||||
import src.utils.consts
|
||||
from src.models.requests import LinkRequest, LinkResponse, Solution
|
||||
from src.consts import LOG_LEVEL, VERSION
|
||||
from src.endpoints import router
|
||||
from src.middlewares import LogRequest
|
||||
from src.utils import logger
|
||||
from src.utils.consts import LOG_LEVEL
|
||||
|
||||
logger.info("Using version %s", VERSION)
|
||||
|
||||
app = FastAPI(debug=LOG_LEVEL == logging.DEBUG, log_level=LOG_LEVEL)
|
||||
app.add_middleware(GZipMiddleware)
|
||||
app.add_middleware(LogRequest)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def read_root():
|
||||
"""Redirect to /docs."""
|
||||
logger.info("Redirecting to /docs")
|
||||
return RedirectResponse(url="/docs", status_code=301)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""Health check endpoint."""
|
||||
logger.info("Health check")
|
||||
|
||||
health_check_request = read_item(
|
||||
LinkRequest.model_construct(url="https://prowlarr.servarr.com/v1/ping")
|
||||
)
|
||||
if health_check_request.solution.status != HTTPStatus.OK:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Health check failed",
|
||||
)
|
||||
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@app.post("/v1")
|
||||
def read_item(request: LinkRequest):
|
||||
"""Handle POST requests."""
|
||||
start_time = int(time.time() * 1000)
|
||||
# request.url = "https://nowsecure.nl"
|
||||
logger.info(f"Request: {request}")
|
||||
response: LinkResponse
|
||||
|
||||
# start_time = int(time.time() * 1000)
|
||||
try:
|
||||
with SB(uc=True, locale_code="en", test=False, xvfb=True, ad_block=True) as sb:
|
||||
sb: BaseCase
|
||||
sb.uc_open_with_reconnect(request.url)
|
||||
source = sb.get_page_source()
|
||||
source_bs = BeautifulSoup(source, "html.parser")
|
||||
title_tag = source_bs.title
|
||||
logger.info(f"Got webpage: {request.url}")
|
||||
if title_tag and title_tag.string in src.utils.consts.CHALLENGE_TITLES:
|
||||
logger.info("Challenge detected")
|
||||
sb.uc_gui_click_captcha()
|
||||
logger.info("Clicked captcha")
|
||||
|
||||
source = sb.get_page_source()
|
||||
source_bs = BeautifulSoup(source, "html.parser")
|
||||
title_tag = source_bs.title
|
||||
|
||||
if title_tag and title_tag.string in src.utils.consts.CHALLENGE_TITLES:
|
||||
sb.save_screenshot(f"./screenshots/{request.url}.png")
|
||||
raise_captcha_bypass_error()
|
||||
|
||||
response = LinkResponse(
|
||||
message="Success",
|
||||
solution=Solution(
|
||||
userAgent=sb.get_user_agent(),
|
||||
url=sb.get_current_url(),
|
||||
status=200,
|
||||
cookies=sb.get_cookies(),
|
||||
headers={},
|
||||
response=source,
|
||||
),
|
||||
startTimestamp=start_time,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error: {e}")
|
||||
raise HTTPException(status_code=500, detail="Unknown error, check logs") from e
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def raise_captcha_bypass_error():
|
||||
"""
|
||||
Raise a 500 error if the challenge could not be bypassed.
|
||||
|
||||
This function should be called if the challenge is not bypassed after
|
||||
clicking the captcha.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
raise HTTPException(status_code=500, detail="Could not bypass challenge")
|
||||
app.include_router(router=router)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Generated
-2239
File diff suppressed because it is too large
Load Diff
@@ -1,3 +0,0 @@
|
||||
[virtualenvs]
|
||||
in-project = true
|
||||
create = true
|
||||
+23
-30
@@ -1,38 +1,31 @@
|
||||
[tool.poetry]
|
||||
# cspell:disable
|
||||
[project]
|
||||
authors = [{ name = "ThePhaseless", email = "kukubaorch@gmail.com" }]
|
||||
license = { text = "LICENSE" }
|
||||
requires-python = "<4.0,>=3.12"
|
||||
name = "Byparr"
|
||||
version = "0.1.0"
|
||||
description = "API for getting cookies for Cloudflare challenges"
|
||||
package-mode = false
|
||||
authors = ["ThePhaseless <kukubaorch@gmail.com>"]
|
||||
readme = "README.md"
|
||||
license = "LICENSE"
|
||||
repository = "https://github.com/ThePhaseless/Byparr"
|
||||
dependencies = [
|
||||
"fastapi[standard]==0.115.9",
|
||||
"pyautogui==0.9.54",
|
||||
"pydantic==2.10.6",
|
||||
"seleniumbase==4.35.2",
|
||||
"uvicorn==0.34.0",
|
||||
]
|
||||
urls = { repository = "https://github.com/ThePhaseless/Byparr" }
|
||||
|
||||
# cspell:disable
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.12"
|
||||
fastapi = { extras = ["standard"], version = "^0" }
|
||||
seleniumbase = "^4.33.1"
|
||||
beautifulsoup4 = "^4.12.3"
|
||||
uvicorn = "^0.32.1"
|
||||
pydantic = "^2.10.2"
|
||||
pyautogui = "^0.9.54"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
ruff = "^0.8.0"
|
||||
deptry = "^0.21.1"
|
||||
|
||||
[tool.poetry.group.test.dependencies]
|
||||
pytest = "^8.3.3"
|
||||
httpx = "^0.27.2"
|
||||
pytest-retry = "^1.6.3"
|
||||
pytest-progress = "^1.3.0"
|
||||
pytest-asyncio = "^0"
|
||||
pytest-xdist = "^3.6.1"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
[dependency-groups]
|
||||
test = [
|
||||
"httpx==0.28.1",
|
||||
"pytest==8.3.4",
|
||||
"pytest-asyncio==0.25.3",
|
||||
"pytest-progress==1.3.0",
|
||||
"pytest-retry==1.7.0",
|
||||
"pytest-xdist==3.6.1",
|
||||
]
|
||||
dev = ["deptry==0.23.0", "ruff==0.9.8"]
|
||||
|
||||
[tool.deptry.per_rule_ignores]
|
||||
DEP002 = ["pyautogui"]
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
PROXY = os.getenv("PROXY")
|
||||
|
||||
CHALLENGE_TITLES = [
|
||||
# Cloudflare
|
||||
"Just a moment...",
|
||||
# DDoS-GUARD
|
||||
"DDoS-Guard",
|
||||
]
|
||||
Executable
+75
@@ -0,0 +1,75 @@
|
||||
import time
|
||||
from http import HTTPStatus
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.responses import RedirectResponse
|
||||
from sbase import BaseCase
|
||||
|
||||
from src.consts import CHALLENGE_TITLES
|
||||
from src.models import (
|
||||
LinkRequest,
|
||||
LinkResponse,
|
||||
Solution,
|
||||
)
|
||||
|
||||
from .utils import get_sb, logger, save_screenshot
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
SeleniumDep = Annotated[BaseCase, Depends(get_sb)]
|
||||
|
||||
|
||||
@router.get("/", include_in_schema=False)
|
||||
def read_root():
|
||||
"""Redirect to /docs."""
|
||||
logger.debug("Redirecting to /docs")
|
||||
return RedirectResponse(url="/docs", status_code=301)
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
def health_check(sb: SeleniumDep):
|
||||
"""Health check endpoint."""
|
||||
health_check_request = read_item(
|
||||
LinkRequest.model_construct(url="https://google.com"),
|
||||
sb,
|
||||
)
|
||||
|
||||
if health_check_request.solution.status != HTTPStatus.OK:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Health check failed",
|
||||
)
|
||||
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.post("/v1")
|
||||
def read_item(request: LinkRequest, sb: SeleniumDep) -> 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")
|
||||
|
||||
return LinkResponse(
|
||||
message="Success",
|
||||
solution=Solution(
|
||||
userAgent=sb.get_user_agent(),
|
||||
url=sb.get_current_url(),
|
||||
status=200,
|
||||
cookies=sb.get_cookies(),
|
||||
headers={},
|
||||
response=str(sb.get_beautiful_soup()),
|
||||
),
|
||||
start_timestamp=start_time,
|
||||
)
|
||||
@@ -0,0 +1,29 @@
|
||||
import time
|
||||
from http import HTTPStatus
|
||||
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
|
||||
from src.models import LinkRequest
|
||||
from src.utils import logger
|
||||
|
||||
|
||||
class LogRequest(BaseHTTPMiddleware):
|
||||
async def dispatch(self, request, call_next):
|
||||
"""Log requests."""
|
||||
if request.url.path != "/v1":
|
||||
return await call_next(request)
|
||||
|
||||
start_time = time.perf_counter()
|
||||
request_body = LinkRequest.model_validate(await request.json())
|
||||
logger.info(
|
||||
f"From: {request.client.host if request.client else 'unknown'} at {time.strftime('%Y-%m-%d %H:%M:%S')}: {request_body.url}"
|
||||
)
|
||||
response = await call_next(request)
|
||||
process_time = time.perf_counter() - start_time
|
||||
|
||||
if response.status_code == HTTPStatus.OK:
|
||||
logger.info(f"Done {request_body.url} in {process_time:.2f}s")
|
||||
else:
|
||||
logger.info(f"Failed {request_body.url} in {process_time:.2f}s")
|
||||
|
||||
return response
|
||||
@@ -0,0 +1,73 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from http import HTTPStatus
|
||||
from typing import Annotated, Any
|
||||
|
||||
from fastapi import Body
|
||||
from pydantic import BaseModel
|
||||
|
||||
from src import consts
|
||||
|
||||
|
||||
class LinkRequest(BaseModel):
|
||||
cmd: Annotated[
|
||||
str,
|
||||
Body(
|
||||
default="request.get",
|
||||
description="Type of request, currently only supports GET requests. This string is purely for compatibility with FlareSolverr.",
|
||||
),
|
||||
]
|
||||
url: Annotated[str, Body(pattern=r"^https?://", default="https://")]
|
||||
max_timeout: Annotated[int, Body(default=60)]
|
||||
|
||||
|
||||
class Solution(BaseModel):
|
||||
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="",
|
||||
)
|
||||
|
||||
|
||||
class LinkResponse(BaseModel):
|
||||
status: str = "ok"
|
||||
message: str
|
||||
solution: Solution
|
||||
start_timestamp: Annotated[int, Body(alias="startTimestamp")] = int(
|
||||
time.time() * 1000
|
||||
)
|
||||
end_timestamp: Annotated[int, Body(alias="endTimestamp")] = int(time.time() * 1000)
|
||||
version: str = consts.VERSION
|
||||
|
||||
@classmethod
|
||||
def invalid(cls, url: str):
|
||||
"""
|
||||
Return an invalid LinkResponse with default error values.
|
||||
|
||||
This method is used to generate a response indicating an invalid request.
|
||||
"""
|
||||
return cls(
|
||||
status="error",
|
||||
message="Invalid request",
|
||||
solution=Solution.invalid(url),
|
||||
start_timestamp=int(time.time() * 1000),
|
||||
end_timestamp=int(time.time() * 1000),
|
||||
)
|
||||
@@ -1,95 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Author(BaseModel):
|
||||
login: str
|
||||
id: int
|
||||
node_id: str
|
||||
avatar_url: str
|
||||
gravatar_id: str
|
||||
url: str
|
||||
html_url: str
|
||||
followers_url: str
|
||||
following_url: str
|
||||
gists_url: str
|
||||
starred_url: str
|
||||
subscriptions_url: str
|
||||
organizations_url: str
|
||||
repos_url: str
|
||||
events_url: str
|
||||
received_events_url: str
|
||||
type: str
|
||||
site_admin: bool
|
||||
|
||||
|
||||
class Uploader(BaseModel):
|
||||
login: str
|
||||
id: int
|
||||
node_id: str
|
||||
avatar_url: str
|
||||
gravatar_id: str
|
||||
url: str
|
||||
html_url: str
|
||||
followers_url: str
|
||||
following_url: str
|
||||
gists_url: str
|
||||
starred_url: str
|
||||
subscriptions_url: str
|
||||
organizations_url: str
|
||||
repos_url: str
|
||||
events_url: str
|
||||
received_events_url: str
|
||||
type: str
|
||||
site_admin: bool
|
||||
|
||||
|
||||
class Asset(BaseModel):
|
||||
url: str
|
||||
id: int
|
||||
node_id: str
|
||||
name: str
|
||||
label: str | None
|
||||
uploader: Uploader
|
||||
content_type: str
|
||||
state: str
|
||||
size: int
|
||||
download_count: int
|
||||
created_at: str
|
||||
updated_at: str
|
||||
browser_download_url: str
|
||||
|
||||
|
||||
class Reactions(BaseModel):
|
||||
url: str
|
||||
total_count: int
|
||||
|
||||
laugh: int
|
||||
hooray: int
|
||||
confused: int
|
||||
heart: int
|
||||
rocket: int
|
||||
eyes: int
|
||||
|
||||
|
||||
class GithubResponse(BaseModel):
|
||||
url: str
|
||||
assets_url: str
|
||||
upload_url: str
|
||||
html_url: str
|
||||
id: int
|
||||
author: Author
|
||||
node_id: str
|
||||
tag_name: str
|
||||
target_commitish: str
|
||||
name: str
|
||||
draft: bool
|
||||
prerelease: bool
|
||||
created_at: str
|
||||
published_at: str
|
||||
assets: list[Asset]
|
||||
tarball_url: str
|
||||
zipball_url: str
|
||||
body: str
|
||||
reactions: Reactions
|
||||
@@ -1,61 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from http import HTTPStatus
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class LinkRequest(BaseModel):
|
||||
cmd: str = "get"
|
||||
url: str
|
||||
max_timeout: int = Field(30, alias="maxTimeout")
|
||||
|
||||
|
||||
class ProtectionTriggeredError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Solution(BaseModel):
|
||||
url: str
|
||||
status: int
|
||||
cookies: list
|
||||
userAgent: str # noqa: N815 # Ignore to preserve compatibility
|
||||
headers: dict[str, Any]
|
||||
response: str
|
||||
|
||||
@classmethod
|
||||
def empty(cls):
|
||||
return cls(
|
||||
url="",
|
||||
status=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
cookies=[],
|
||||
userAgent="",
|
||||
headers={},
|
||||
response="",
|
||||
)
|
||||
|
||||
|
||||
class LinkResponse(BaseModel):
|
||||
status: str = "ok"
|
||||
message: str
|
||||
solution: Solution
|
||||
startTimestamp: int # noqa: N815 # Ignore to preserve compatibility
|
||||
endTimestamp: int = int(time.time() * 1000) # noqa: N815 # Ignore to preserve compatibility
|
||||
version: str = "3.3.21" # TODO: Implement versioning
|
||||
|
||||
@classmethod
|
||||
def invalid(cls):
|
||||
return cls(
|
||||
status="error",
|
||||
message="Invalid request",
|
||||
solution=Solution.empty(),
|
||||
startTimestamp=int(time.time() * 1000),
|
||||
endTimestamp=int(time.time() * 1000),
|
||||
version="3.3.21",
|
||||
)
|
||||
|
||||
|
||||
class NoChromeExtensionError(Exception):
|
||||
"""No chrome extention found."""
|
||||
@@ -0,0 +1,35 @@
|
||||
import logging
|
||||
from time import gmtime, strftime
|
||||
|
||||
from fastapi import Header
|
||||
from sbase import SB, BaseCase
|
||||
|
||||
from src.consts import LOG_LEVEL, PROXY, USE_HEADLESS
|
||||
|
||||
logger = logging.getLogger("uvicorn.error")
|
||||
logger.setLevel(LOG_LEVEL)
|
||||
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
|
||||
|
||||
|
||||
def save_screenshot(sb: BaseCase):
|
||||
"""Save screenshot on HTTPException."""
|
||||
sb.save_screenshot(f"screenshots/{strftime('%Y-%m-%d %H:%M:%S', gmtime())}.png")
|
||||
@@ -1,8 +0,0 @@
|
||||
import logging
|
||||
|
||||
from src.utils.consts import LOG_LEVEL
|
||||
|
||||
logger = logging.getLogger("uvicorn.error")
|
||||
logger.setLevel(LOG_LEVEL)
|
||||
if len(logger.handlers) == 0:
|
||||
logger.addHandler(logging.StreamHandler())
|
||||
@@ -1,12 +0,0 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
LOG_LEVEL = os.getenv("LOG_LEVEL") or "INFO"
|
||||
LOG_LEVEL = logging.getLevelNamesMapping()[LOG_LEVEL.upper()]
|
||||
|
||||
CHALLENGE_TITLES = [
|
||||
# Cloudflare
|
||||
"Just a moment...",
|
||||
# DDoS-GUARD
|
||||
"DDoS-Guard",
|
||||
]
|
||||
+7
-12
@@ -5,26 +5,18 @@ import pytest
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from main import app
|
||||
from src.models.requests import LinkRequest
|
||||
from src.models import LinkRequest
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
test_websites = [
|
||||
"https://btmet.com/",
|
||||
"https://ext.to/",
|
||||
]
|
||||
|
||||
# Not testable on github actions
|
||||
github_restricted = [
|
||||
"https://www.ygg.re/",
|
||||
"https://extratorrent.st/",
|
||||
"https://idope.se/",
|
||||
"https://speed.cd/login",
|
||||
]
|
||||
|
||||
# if os.getenv("GITHUB_ACTIONS") != "true":
|
||||
test_websites.extend(github_restricted)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("website", test_websites)
|
||||
def test_bypass(website: str):
|
||||
@@ -44,9 +36,12 @@ def test_bypass(website: str):
|
||||
|
||||
response = client.post(
|
||||
"/v1",
|
||||
json=LinkRequest.model_construct(
|
||||
url=website, max_timeout=30, cmd="request.get"
|
||||
).model_dump(),
|
||||
json={
|
||||
**LinkRequest.model_construct(
|
||||
url=website, max_timeout=30, cmd="request.get"
|
||||
).model_dump(),
|
||||
# "proxy": "203.174.15.83:8080",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
|
||||
Reference in New Issue
Block a user