commit 85b28835d1933da212c3a180c1fe7a049bb8140a Author: protokey Date: Tue Sep 8 15:53:19 2026 +0400 chore: init diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..b33fd16 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = tab +insert_final_newline = true +max_line_length = 120 +tab_width = 4 + +[{*.har,*.jsb2,*.jsb3,*.json,*.jsonc,*.postman_collection,*.postman_collection.json,*.postman_environment,*.postman_environment.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,.ws-context,jest.config}] +indent_size = 2 + +[{*.yaml,*.yml,environment.yaml,environment.yml}] +indent_size = 2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0f00739 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +extra/ +.venv/ +.idea/ +wheels/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2024224 --- /dev/null +++ b/Makefile @@ -0,0 +1,51 @@ +VENV := .venv +PYTHON := $(VENV)/bin/python +PIP := $(VENV)/bin/pip +WHEELS_DIR := wheels +REQUIREMENTS := requirements.txt + +MAKECMDGOALS_TARGETS := install freeze wheels sync sync-offline clean help +PKG := $(filter-out $(MAKECMDGOALS_TARGETS),$(MAKECMDGOALS)) + +.PHONY: install freeze wheels sync sync-offline clean help venv + +help: + @echo "make install [ ...] - установить пакет(ы) в $(VENV), обновить $(REQUIREMENTS), собрать wheel в $(WHEELS_DIR)/" + @echo "make sync - установить все пакеты из $(REQUIREMENTS) (из индекса)" + @echo "make sync-offline - установить все пакеты из $(REQUIREMENTS) только из $(WHEELS_DIR)/, без обращения к индексу" + @echo "make freeze - перезаписать $(REQUIREMENTS) текущим состоянием окружения" + @echo "make wheels - собрать wheel для всех пакетов из $(REQUIREMENTS)" + @echo "make clean - удалить $(WHEELS_DIR)/" + +venv: + @test -d $(VENV) || python3 -m venv $(VENV) + +install: venv +ifeq ($(strip $(PKG)),) + $(error Укажите пакет: make install , например make install redis) +endif + $(PIP) install --upgrade $(PKG) + $(PIP) freeze > $(REQUIREMENTS) + @mkdir -p $(WHEELS_DIR) + $(PIP) wheel $(PKG) -w $(WHEELS_DIR) + @echo "Готово: $(PKG) установлен, $(REQUIREMENTS) обновлён, wheel-пакеты в $(WHEELS_DIR)/" + +sync: venv + $(PIP) install -r $(REQUIREMENTS) + +sync-offline: venv + $(PIP) install --no-index --find-links=$(WHEELS_DIR) -r $(REQUIREMENTS) + +freeze: venv + $(PIP) freeze > $(REQUIREMENTS) + +wheels: venv + @mkdir -p $(WHEELS_DIR) + $(PIP) wheel -r $(REQUIREMENTS) -w $(WHEELS_DIR) + +clean: + rm -rf $(WHEELS_DIR) + +# позволяет передавать имя пакета как аргумент: make install redis +%: + @: diff --git a/README.md b/README.md new file mode 100644 index 0000000..3d12d19 --- /dev/null +++ b/README.md @@ -0,0 +1,137 @@ +# WebRobo API + +Browser automation API with built-in anti-bot stealth. Send a URL and a sequence of declarative actions — fill a field, +click, wait for an element — and get back the rendered page, including sites behind Cloudflare-style JS challenges. + +## Status + +Early MVP (v0), under active development. Single browser engine, no queue, no billing yet — see [Roadmap](#roadmap). + +## Architecture + +``` +Client → Caddy (TLS) → FastAPI → API-key auth → semaphore acquire + │ + ▼ + Camoufox browser context + (run actions, collect result) + │ + ▼ + release semaphore → response +``` + +- **Engine**: [Camoufox](https://camoufox.com/) (stealth Firefox, driven via Playwright). Chosen for v1 because + Firefox-based fingerprinting is less commoditized against anti-bot vendors than patched Chromium. +- **API**: FastAPI, single versioned endpoint (`POST /v1/solve`). +- **Concurrency**: bounded by an `asyncio.Semaphore` over browser contexts — no external job queue in v1. Simpler to + run, hard ceiling on throughput per instance (see Roadmap for scaling out). +- **Auth**: per-client API key (`X-API-Key` header), checked against a SQLite table. (SQLAlchemy ORM) +- **Logging**: every request's outcome (success/failure/duration) is persisted — this becomes the success-rate data used + to evaluate the service and talk to clients about reliability. +- **Proxies**: not managed by the service in v1 — the client supplies their own proxy per request if they need one. +- **Deployment**: single Docker container behind a Caddy reverse proxy. + +### Planned project layout + +``` +src/ + main.py # FastAPI app + router wiring + api/ + solve.py # POST /v1/solve + deps.py # API-key auth dependency + core/ + config.py # settings (env vars) + engine/ + browser.py # Camoufox context pool + semaphore + actions.py # fill / click / wait_for execution + schemas.py # SolveRequest / SolveResponse / Action models + db/ + models.py # api_keys, request_logs + session.py +tests/ +wheels/ # whl библиотеки для оффлайн установки под Ubuntu Noble +Dockerfile +requirements.txt +env.example +``` + +## API (draft) + +``` +POST /v1/solve +X-API-Key: + +{ + "captcha": "token", + "url": "https://example.com/login", + "actions": [ + {"type": "fill", "selector": "#email", "value": "user@example.com"}, + {"type": "fill", "selector": "#password", "value": "..."}, + {"type": "click", "selector": "#submit"}, + {"type": "wait_for", "selector": ".dashboard", "timeout_ms": 30000} + ], + "proxy": "http://user:pass@host:port", + "timeout_ms": 120000 +} +``` + +Response: + +```json +{ + "status": "ok", + "html": "...", + "cookies": [ + { + "name": "session", + "value": "..." + } + ], + "actions_result": [ + { + "type": "fill", + "ok": true + }, + { + "type": "fill", + "ok": true + }, + { + "type": "click", + "ok": true + }, + { + "type": "wait_for", + "ok": true + } + ], + "error": null +} +``` + +On failure, `status` is `"error"` and `error` carries a machine-readable reason (`selector_not_found`, `timeout`, +`navigation_failed`, ...). + +Fill команда включает 3 действия + +1. `{"type": "locate", "selector": "#email"},` - навести мышку на элемент +2. `{"type": "click"},` - клик в текущие координаты +3. `{"type": "typing", "value": "user@example.com"},` - набор текста + +## Roadmap + +Deliberately out of scope for v1: + +- Second engine (Patchright/Chromium) for targets where the Firefox fingerprint doesn't fit +- Job queue (Redis/RabbitMQ) + multiple worker VPS for horizontal scaling +- Real billing/usage plans beyond a flat API-key check +- Broader action vocabulary beyond fill/click/wait_for + +## Local development + +```bash +python -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +uvicorn app.main:app --reload +``` diff --git a/env.example b/env.example new file mode 100644 index 0000000..6a329b3 --- /dev/null +++ b/env.example @@ -0,0 +1 @@ +# Project Env diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..28692be --- /dev/null +++ b/requirements.txt @@ -0,0 +1,61 @@ +aiohappyeyeballs==2.7.1 +aiohttp==3.14.3 +aiosignal==1.4.0 +annotated-doc==0.0.5 +annotated-types==0.8.0 +anyio==4.15.1 +apify_fingerprint_datapoints==0.15.0 +attrs==26.1.0 +blessed==1.49.0 +browserforge==1.2.4 +camoufox==0.5.6 +certifi==2026.7.22 +charset-normalizer==3.5.1 +click==8.5.0 +editor==1.8.0 +fastapi==0.141.1 +frozenlist==1.8.0 +geoip2==5.3.0 +greenlet==3.5.5 +h11==0.16.0 +idna==3.19 +inquirer==3.4.1 +jinxed==2.1.0 +language-tags==1.3.1 +lxml==6.1.3 +markdown-it-py==4.2.0 +maxminddb==3.1.1 +mdurl==0.1.2 +multidict==6.7.1 +numpy==2.5.3 +orjson==3.12.0 +platformdirs==4.11.7 +playwright==1.62.0 +propcache==0.5.2 +pydantic==2.13.5 +pydantic_core==2.46.5 +pyee==13.0.1 +Pygments==2.21.0 +PySide6==6.11.2 +PySide6_Addons==6.11.2 +PySide6_Essentials==6.11.2 +PySocks==1.7.1 +PyYAML==6.0.3 +readchar==4.2.2 +redis==8.1.0 +requests==2.34.2 +rich==15.0.0 +rich-click==1.9.9 +runs==1.3.0 +screeninfo==0.8.1 +shiboken6==6.11.2 +starlette==1.6.0 +typing-inspection==0.4.4 +typing_extensions==4.16.0 +ua-parser==1.0.2 +ua-parser-builtins==202606 +urllib3==2.7.0 +uvicorn==0.52.4 +wcwidth==0.8.3 +xmod==1.10.0 +yarl==1.24.5 diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..6d7c6d9 --- /dev/null +++ b/src/main.py @@ -0,0 +1,13 @@ +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +async def root(): + return {"message": "Hello World"} + + +@app.get("/hello/{name}") +async def say_hello(name: str): + return {"message": f"Hello {name}"} diff --git a/test/test_main.http b/test/test_main.http new file mode 100644 index 0000000..a2d81a9 --- /dev/null +++ b/test/test_main.http @@ -0,0 +1,11 @@ +# Test your FastAPI endpoints + +GET http://127.0.0.1:8000/ +Accept: application/json + +### + +GET http://127.0.0.1:8000/hello/User +Accept: application/json + +###