diff --git a/README.md b/README.md index 19b8433..52e9ac7 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,13 @@ 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). -> [!WARNING] +> [!CAUTION] +> From now on, Byparr will clear all chrome processes longer than specified in `MAX_CHROME_LIFETIME` environment variable (default is 300 seconds). To disable this behavior, set `MAX_CHROME_LIFETIME` to `0`. + +> [!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] @@ -27,15 +30,7 @@ An alternative to [FlareSolverr](https://github.com/FlareSolverr/FlareSolverr) a ### 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 `docker-compose.yaml` ### Docker diff --git a/docker-compose.yaml b/docker-compose.yaml index 469dead..9a38fd4 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -6,5 +6,6 @@ services: dockerfile: Dockerfile environment: - LOG_LEVEL=INFO + - MAX_CHROME_LIFETIME=300 ports: - "8191:8191" diff --git a/main.py b/main.py index bf2bd8f..4b6dfa5 100644 --- a/main.py +++ b/main.py @@ -12,10 +12,11 @@ from sbase import SB, BaseCase import src import src.utils +from src.utils import consts import src.utils.consts from src.models.requests import LinkRequest, LinkResponse, Solution from src.utils import logger -from src.utils.consts import LOG_LEVEL +from src.utils.consts import LOG_LEVEL, kill_chromium_processes app = FastAPI(debug=LOG_LEVEL == logging.DEBUG, log_level=LOG_LEVEL) @@ -25,18 +26,19 @@ cookies = [] @app.get("/") def read_root(): """Redirect to /docs.""" - logger.info("Redirecting to /docs") + logger.debug("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 consts.MAX_CHROME_LIFETIME>0: + kill_chromium_processes() + if health_check_request.solution.status != HTTPStatus.OK: raise HTTPException( status_code=500, @@ -66,9 +68,9 @@ def read_item(request: LinkRequest) -> LinkResponse: source = sb.get_page_source() source_bs = BeautifulSoup(source, "html.parser") title_tag = source_bs.title - logger.info(f"Got webpage: {request.url}") + logger.debug(f"Got webpage: {request.url}") if title_tag and title_tag.string in src.utils.consts.CHALLENGE_TITLES: - logger.info("Challenge detected") + logger.debug("Challenge detected") sb.uc_gui_click_captcha() logger.info("Clicked captcha") diff --git a/pyproject.toml b/pyproject.toml index 4280748..df383d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ readme = "README.md" dependencies = [ "beautifulsoup4==4.12.3", "fastapi[standard]==0.115.6", + "psutil==6.1.0", "pyautogui==0.9.54", "pydantic==2.10.3", "seleniumbase==4.33.10", diff --git a/src/utils/consts.py b/src/utils/consts.py index 8092395..d291964 100644 --- a/src/utils/consts.py +++ b/src/utils/consts.py @@ -1,5 +1,10 @@ import logging import os +import time + +import psutil + +from src.utils import logger def get_version_from_env(): @@ -21,12 +26,38 @@ def get_version_from_env(): return version_env.removeprefix("v") +def kill_chromium_processes(): + # Define the prefix and time threshold + prefix = "chromium" + time_threshold = 300 # 5 minutes in seconds + + # Get the current time + current_time = time.time() + + # Iterate through all processes + for proc in psutil.process_iter(['pid', 'name', 'create_time']): + try: + # Extract process details + pid = proc.info['pid'] + name:str = proc.info['name'] + create_time = proc.info['create_time'] + + # Check if the process name starts with the prefix and has been running longer than the threshold + if name and name.startswith(prefix) and (current_time - create_time > time_threshold): + logger.info(f"Terminating process {name} (PID: {pid}) running for {int(current_time - create_time)} seconds") + psutil.Process(pid).terminate() # Terminate the process + + except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): + # Ignore processes that no longer exist or can't be accessed + pass LOG_LEVEL = os.getenv("LOG_LEVEL") or "INFO" LOG_LEVEL = logging.getLevelNamesMapping()[LOG_LEVEL.upper()] VERSION = get_version_from_env() or "unknown" +MAX_CHROME_LIFETIME= int(os.getenv("MAX_CHROME_LIFETIME", 300)) + CHALLENGE_TITLES = [ # Cloudflare diff --git a/uv.lock b/uv.lock index 8e0e9c2..5977d67 100644 --- a/uv.lock +++ b/uv.lock @@ -66,6 +66,7 @@ source = { virtual = "." } dependencies = [ { name = "beautifulsoup4" }, { name = "fastapi", extra = ["standard"] }, + { name = "psutil" }, { name = "pyautogui" }, { name = "pydantic" }, { name = "seleniumbase" }, @@ -90,6 +91,7 @@ test = [ requires-dist = [ { name = "beautifulsoup4", specifier = "==4.12.3" }, { name = "fastapi", extras = ["standard"], specifier = "==0.115.6" }, + { name = "psutil", specifier = ">=6.1.0" }, { name = "pyautogui", specifier = "==0.9.54" }, { name = "pydantic", specifier = "==2.10.3" }, { name = "seleniumbase", specifier = "==4.33.10" }, @@ -594,6 +596,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, ] +[[package]] +name = "psutil" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/10/2a30b13c61e7cf937f4adf90710776b7918ed0a9c434e2c38224732af310/psutil-6.1.0.tar.gz", hash = "sha256:353815f59a7f64cdaca1c0307ee13558a0512f6db064e92fe833784f08539c7a", size = 508565 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/9e/8be43078a171381953cfee33c07c0d628594b5dbfc5157847b85022c2c1b/psutil-6.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6e2dcd475ce8b80522e51d923d10c7871e45f20918e027ab682f94f1c6351688", size = 247762 }, + { url = "https://files.pythonhosted.org/packages/1d/cb/313e80644ea407f04f6602a9e23096540d9dc1878755f3952ea8d3d104be/psutil-6.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0895b8414afafc526712c498bd9de2b063deaac4021a3b3c34566283464aff8e", size = 248777 }, + { url = "https://files.pythonhosted.org/packages/65/8e/bcbe2025c587b5d703369b6a75b65d41d1367553da6e3f788aff91eaf5bd/psutil-6.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dcbfce5d89f1d1f2546a2090f4fcf87c7f669d1d90aacb7d7582addece9fb38", size = 284259 }, + { url = "https://files.pythonhosted.org/packages/58/4d/8245e6f76a93c98aab285a43ea71ff1b171bcd90c9d238bf81f7021fb233/psutil-6.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:498c6979f9c6637ebc3a73b3f87f9eb1ec24e1ce53a7c5173b8508981614a90b", size = 287255 }, + { url = "https://files.pythonhosted.org/packages/27/c2/d034856ac47e3b3cdfa9720d0e113902e615f4190d5d1bdb8df4b2015fb2/psutil-6.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d905186d647b16755a800e7263d43df08b790d709d575105d419f8b6ef65423a", size = 288804 }, + { url = "https://files.pythonhosted.org/packages/ea/55/5389ed243c878725feffc0d6a3bc5ef6764312b6fc7c081faaa2cfa7ef37/psutil-6.1.0-cp37-abi3-win32.whl", hash = "sha256:1ad45a1f5d0b608253b11508f80940985d1d0c8f6111b5cb637533a0e6ddc13e", size = 250386 }, + { url = "https://files.pythonhosted.org/packages/11/91/87fa6f060e649b1e1a7b19a4f5869709fbf750b7c8c262ee776ec32f3028/psutil-6.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:a8fb3752b491d246034fa4d279ff076501588ce8cbcdbb62c32fd7a377d996be", size = 254228 }, +] + [[package]] name = "pyautogui" version = "0.9.54"