diff --git a/README.md b/README.md index ce96146..0c03b90 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ X-API-Key: {"type": "wait_for", "selector": ".dashboard", "timeout": 30} ], "proxy": "socks5://user:pass@host:port", + "screen": "1280x920", "user_agent": "Firefox", "time_zone": "", diff --git a/docs/Introduction.md b/docs/Introduction.md index 3c65bdd..dae466c 100644 --- a/docs/Introduction.md +++ b/docs/Introduction.md @@ -10,7 +10,7 @@ - proxy - обязательно нужно задать прокси, система не станет палить свою инфраструктуру - time_zone - временная зона - locale - языковая настройка -- user_agent - заголовок браузера, если не передать будет использован свой +- user_agent - заголовок браузера Firefox, если не передать будет использован свой - screen - размер окна, по умолчанию: 1280х920 - timeout - общий таймаут выполнения запроса, по умолчанию: 120 - captcha - токен сервиса распознавания, если требуется решить капчу на странице diff --git a/src/engine/schemas.py b/src/engine/schemas.py index 84b2771..483c4fa 100644 --- a/src/engine/schemas.py +++ b/src/engine/schemas.py @@ -2,6 +2,10 @@ from pydantic import BaseModel class ActionRequest(BaseModel): + """ + Действие автоматизации. Характеризуется типом и производится над селектором. + Набор параметров зависит от типа, этот класс перечисляет все параметры. + """ type: str selector: str | None = None value: str | None = None @@ -9,6 +13,11 @@ class ActionRequest(BaseModel): class SolveRequest(BaseModel): + """ + # TODO: добавить locale и time_zone + + Объект запроса для автоматизации на сайте. + """ url: str proxy: str actions: list[ActionRequest] diff --git a/src/main.py b/src/main.py index eaded3f..54fce69 100644 --- a/src/main.py +++ b/src/main.py @@ -1,12 +1,36 @@ +import uuid from datetime import datetime +from pathlib import Path +from typing import Annotated +from urllib.parse import urlsplit +from browserforge.fingerprints import Screen from camoufox.async_api import AsyncCamoufox +from camoufox.utils import determine_ua_os +from fastapi import Cookie from fastapi import FastAPI from fastapi import Request +from fastapi import Response from playwright.async_api import ViewportSize, ProxySettings +from ua_parser import user_agent_parser -from engine.schemas import SolveRequest from engine import actions +from engine.schemas import SolveRequest + +BROWSER_SLOTS = 10 + +IDENTITY_FILE = Path("identity.json") + + +def parse_proxy(proxy_url: str) -> ProxySettings: + parts = urlsplit(proxy_url) + server = f"{parts.scheme}://{parts.hostname}:{parts.port}" + settings: ProxySettings = {"server": server} + if parts.username: + settings["username"] = parts.username + if parts.password: + settings["password"] = parts.password + return settings app = FastAPI() @@ -17,28 +41,71 @@ async def root(): return {"message": "Hello World"} +_OS_MAP = {'win': 'windows', 'mac': 'macos', 'lin': 'linux'} + + +def parse_os_and_ff(user_agent): + os_name = _OS_MAP[determine_ua_os(user_agent)] + parsed = user_agent_parser.ParseUserAgent(user_agent) + ff_version = int(parsed["major"]) if( + parsed.get("family") == "Firefox" and parsed.get("major") )else None + return os_name, ff_version + + @app.post("/v1/solve") -async def solve(request: Request, payload: SolveRequest): +async def solve( + request: Request, + payload: SolveRequest, + response: Response, + session_id: Annotated[str | None, Cookie()] = None +): + global BROWSER_SLOTS + + if BROWSER_SLOTS <= 0: + return { + "ok": False, + "error": "No slots available", + } + else: + BROWSER_SLOTS -= 1 + api_key = request.headers.get('X-API-Key') - async with AsyncCamoufox() as camoufo: - # TODO: настройка контекста - scr_w, scr_h = payload.screen.split("x") - scr_size = ViewportSize(width=int(scr_w), height=int(scr_h)) - # TODO: распарс proxy-урл и заполнение объекта - # proxy = ProxySettings(socks5://host:port, username, password) - proxy = None - page = await camoufo.new_page( - user_agent=payload.user_agent, - proxy=proxy, - viewport=scr_size, - screen=scr_size, - ) + + if not session_id: + session_id = str(uuid.uuid4()) + response.set_cookie("session_id", session_id) + + scr_w, scr_h = payload.screen.split("x") + scr_size = int(scr_w), int(scr_h) + # TODO: распарс proxy-урл и заполнение объекта + # proxy = ProxySettings(socks5://host:port, username, password) + proxy = None + + browser_options = {} + if payload.user_agent: + os, ff_version = parse_os_and_ff(payload.user_agent) + browser_options["os"] = os + browser_options["ff_version"] = ff_version + browser_options["config"] = {'navigator.UserAgent': payload.user_agent} + + user_data_dir = Path(__file__).parent / f"../extra/user_data_dir/{session_id}" + if not user_data_dir.exists(): + user_data_dir.mkdir(parents=True) + + async with AsyncCamoufox( + persistent_context=True, + user_data_dir=user_data_dir, + proxy=proxy, + window=scr_size, + **browser_options, + ) as camoufo: + page = camoufo.pages[0] if camoufo.pages else await camoufo.new_page() page.set_default_timeout(30_000) await page.goto(url=payload.url) actions_result = await actions.execute(page, payload.actions) status = not "error" in actions_result[-1] error = ["Something error", None][status] - # cookies = await camoufo.cookies() + cookies = await camoufo.cookies() html = await page.inner_html('html') await page.screenshot( path=( @@ -46,10 +113,12 @@ async def solve(request: Request, payload: SolveRequest): f"{datetime.timestamp(datetime.now())}.png" ) ) + + BROWSER_SLOTS += 1 return { "status": ["err", "ok"][status], "html": html, - # "cookies": cookies, + "cookies": cookies, "actions": actions_result, "error": error, } diff --git a/tests/test_main.http b/tests/test_main.http index 2e101be..538bdae 100644 --- a/tests/test_main.http +++ b/tests/test_main.http @@ -37,6 +37,7 @@ POST http://127.0.0.1:8000/v1/solve Accept: application/json Content-Type: application/json X-API-Key: +Cookie: session_id=b8f770a3-38c2-45f2-944b-2703798e3503; Path=/; SameSite=lax {