veilrender 0.2.0__tar.gz → 0.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. {veilrender-0.2.0 → veilrender-0.3.1}/PKG-INFO +4 -3
  2. {veilrender-0.2.0 → veilrender-0.3.1}/pyproject.toml +4 -2
  3. {veilrender-0.2.0 → veilrender-0.3.1}/src/veilrender/__init__.py +1 -1
  4. {veilrender-0.2.0 → veilrender-0.3.1}/src/veilrender/app.py +5 -1
  5. veilrender-0.3.1/src/veilrender/auth.py +114 -0
  6. {veilrender-0.2.0 → veilrender-0.3.1}/src/veilrender/browser.py +11 -0
  7. veilrender-0.3.1/src/veilrender/config.py +51 -0
  8. veilrender-0.3.1/src/veilrender/data/__init__.py +0 -0
  9. veilrender-0.3.1/src/veilrender/data/blocklist.txt +82621 -0
  10. veilrender-0.3.1/src/veilrender/filters.py +101 -0
  11. {veilrender-0.2.0 → veilrender-0.3.1}/src/veilrender/routes/render.py +31 -1
  12. {veilrender-0.2.0 → veilrender-0.3.1}/src/veilrender/routes/screenshot.py +12 -0
  13. {veilrender-0.2.0 → veilrender-0.3.1}/src/veilrender/stats.py +2 -0
  14. veilrender-0.3.1/src/veilrender/storage.py +197 -0
  15. veilrender-0.3.1/src/veilrender/url_validator.py +123 -0
  16. {veilrender-0.2.0 → veilrender-0.3.1}/src/veilrender.egg-info/PKG-INFO +4 -3
  17. {veilrender-0.2.0 → veilrender-0.3.1}/src/veilrender.egg-info/SOURCES.txt +5 -0
  18. {veilrender-0.2.0 → veilrender-0.3.1}/src/veilrender.egg-info/requires.txt +3 -2
  19. veilrender-0.2.0/src/veilrender/auth.py +0 -39
  20. veilrender-0.2.0/src/veilrender/config.py +0 -25
  21. {veilrender-0.2.0 → veilrender-0.3.1}/README.md +0 -0
  22. {veilrender-0.2.0 → veilrender-0.3.1}/setup.cfg +0 -0
  23. {veilrender-0.2.0 → veilrender-0.3.1}/src/veilrender/__main__.py +0 -0
  24. {veilrender-0.2.0 → veilrender-0.3.1}/src/veilrender/_vendor/__init__.py +0 -0
  25. {veilrender-0.2.0 → veilrender-0.3.1}/src/veilrender/_vendor/benchmark_compare.py +0 -0
  26. {veilrender-0.2.0 → veilrender-0.3.1}/src/veilrender/_vendor/cache.py +0 -0
  27. {veilrender-0.2.0 → veilrender-0.3.1}/src/veilrender/_vendor/config.py +0 -0
  28. {veilrender-0.2.0 → veilrender-0.3.1}/src/veilrender/_vendor/dotenv.py +0 -0
  29. {veilrender-0.2.0 → veilrender-0.3.1}/src/veilrender/_vendor/httpserver.py +0 -0
  30. {veilrender-0.2.0 → veilrender-0.3.1}/src/veilrender/_vendor/jsonc.py +0 -0
  31. {veilrender-0.2.0 → veilrender-0.3.1}/src/veilrender/_vendor/markdown.py +0 -0
  32. {veilrender-0.2.0 → veilrender-0.3.1}/src/veilrender/_vendor/readability.py +0 -0
  33. {veilrender-0.2.0 → veilrender-0.3.1}/src/veilrender/_vendor/retry.py +0 -0
  34. {veilrender-0.2.0 → veilrender-0.3.1}/src/veilrender/_vendor/soup.py +0 -0
  35. {veilrender-0.2.0 → veilrender-0.3.1}/src/veilrender/_vendor/structlog.py +0 -0
  36. {veilrender-0.2.0 → veilrender-0.3.1}/src/veilrender/_vendor/useragent.py +0 -0
  37. {veilrender-0.2.0 → veilrender-0.3.1}/src/veilrender/_vendor/yaml.py +0 -0
  38. {veilrender-0.2.0 → veilrender-0.3.1}/src/veilrender/cdp_proxy.py +0 -0
  39. {veilrender-0.2.0 → veilrender-0.3.1}/src/veilrender/models.py +0 -0
  40. {veilrender-0.2.0 → veilrender-0.3.1}/src/veilrender/routes/__init__.py +0 -0
  41. {veilrender-0.2.0 → veilrender-0.3.1}/src/veilrender/routes/dashboard.py +0 -0
  42. {veilrender-0.2.0 → veilrender-0.3.1}/src/veilrender/routes/health.py +0 -0
  43. {veilrender-0.2.0 → veilrender-0.3.1}/src/veilrender.egg-info/dependency_links.txt +0 -0
  44. {veilrender-0.2.0 → veilrender-0.3.1}/src/veilrender.egg-info/entry_points.txt +0 -0
  45. {veilrender-0.2.0 → veilrender-0.3.1}/src/veilrender.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: veilrender
3
- Version: 0.2.0
3
+ Version: 0.3.1
4
4
  Summary: Headless browser rendering API — self-hostable on HF Spaces, Docker, or bare metal
5
5
  Author: Peng Ding
6
6
  License-Expression: MIT
@@ -12,9 +12,10 @@ Classifier: Topic :: Internet :: WWW/HTTP
12
12
  Requires-Python: >=3.10
13
13
  Description-Content-Type: text/markdown
14
14
  Requires-Dist: cloakbrowser>=0.3.0
15
+ Requires-Dist: minio>=7.2.0
15
16
  Provides-Extra: dev
16
- Requires-Dist: ruff>=0.11.0; extra == "dev"
17
- Requires-Dist: ty>=0.0.1a0; extra == "dev"
17
+ Requires-Dist: ruff==0.15.20; extra == "dev"
18
+ Requires-Dist: ty==0.0.54; extra == "dev"
18
19
  Requires-Dist: pytest>=7.0.0; extra == "dev"
19
20
  Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
20
21
 
@@ -18,12 +18,13 @@ classifiers = [
18
18
 
19
19
  dependencies = [
20
20
  "cloakbrowser>=0.3.0",
21
+ "minio>=7.2.0",
21
22
  ]
22
23
 
23
24
  [project.optional-dependencies]
24
25
  dev = [
25
- "ruff>=0.11.0",
26
- "ty>=0.0.1a0",
26
+ "ruff==0.15.20",
27
+ "ty==0.0.54",
27
28
  "pytest>=7.0.0",
28
29
  "pytest-asyncio>=0.21.0",
29
30
  ]
@@ -43,6 +44,7 @@ version = { attr = "veilrender.__version__" }
43
44
 
44
45
  [tool.setuptools.package-data]
45
46
  "veilrender" = ["py.typed"]
47
+ "veilrender.data" = ["blocklist.txt"]
46
48
 
47
49
  [tool.ruff]
48
50
  target-version = "py310"
@@ -1,3 +1,3 @@
1
1
  """VeilRender — headless browser rendering API."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.3.1"
@@ -12,6 +12,7 @@ from veilrender.browser import browser_manager
12
12
  from veilrender.cdp_proxy import handle_cdp_upgrade, is_websocket_upgrade
13
13
  from veilrender.config import settings
14
14
  from veilrender.routes import dashboard, health, render, screenshot
15
+ from veilrender.storage import storage_manager
15
16
 
16
17
  logging.basicConfig(
17
18
  level=logging.INFO,
@@ -36,7 +37,9 @@ async def _pipe(source: asyncio.StreamReader, dest: asyncio.StreamReader) -> Non
36
37
 
37
38
  def create_app() -> App:
38
39
  """Create and configure the VeilRender application."""
39
- app = App(max_body_size=10 * 1024 * 1024) # 10 MB
40
+ app = App(
41
+ max_body_size=64 * 1024
42
+ ) # 64 KB — render/screenshot payloads are small JSON
40
43
 
41
44
  # Register routes
42
45
  health.register(app)
@@ -53,6 +56,7 @@ def main() -> None:
53
56
 
54
57
  async def run_server() -> None:
55
58
  await browser_manager.start()
59
+ await storage_manager.ensure_bucket()
56
60
 
57
61
  shutdown_event = asyncio.Event()
58
62
 
@@ -0,0 +1,114 @@
1
+ """Token-based authentication with IP-based rate limiting.
2
+
3
+ Failed auth attempts are tracked per IP. After 3 failures within a
4
+ rolling window, the IP is banned for a configurable duration.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import time
11
+ from collections import defaultdict
12
+
13
+ from veilrender._vendor.httpserver import HTTPException, Request
14
+ from veilrender.config import settings
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # Fail2ban settings
19
+ _MAX_FAILURES = 3
20
+ _WINDOW_SECONDS = 300 # 5 minute rolling window
21
+ _BAN_SECONDS = 600 # 10 minute ban
22
+
23
+ # State: per-IP failure timestamps and ban expiry
24
+ _failures: dict[str, list[float]] = defaultdict(list)
25
+ _banned: dict[str, float] = {}
26
+
27
+
28
+ def _get_client_ip(request: Request) -> str:
29
+ """Extract client IP, respecting X-Forwarded-For behind reverse proxy."""
30
+ forwarded = request.headers.get("x-forwarded-for", "")
31
+ if forwarded:
32
+ # Take the first (leftmost) IP — the original client
33
+ return forwarded.split(",")[0].strip()
34
+ return request.client_addr[0] if hasattr(request, "client_addr") else "unknown"
35
+
36
+
37
+ def _is_banned(ip: str) -> bool:
38
+ """Check if an IP is currently banned."""
39
+ ban_until = _banned.get(ip)
40
+ if ban_until is None:
41
+ return False
42
+ if time.monotonic() >= ban_until:
43
+ del _banned[ip]
44
+ return False
45
+ return True
46
+
47
+
48
+ def _record_failure(ip: str) -> None:
49
+ """Record an auth failure and ban if threshold exceeded."""
50
+ now = time.monotonic()
51
+
52
+ # Clean old entries outside the window
53
+ _failures[ip] = [t for t in _failures[ip] if now - t < _WINDOW_SECONDS]
54
+ _failures[ip].append(now)
55
+
56
+ if len(_failures[ip]) >= _MAX_FAILURES:
57
+ _banned[ip] = now + _BAN_SECONDS
58
+ _failures.pop(ip, None)
59
+ logger.warning(
60
+ "IP %s banned for %ds after %d auth failures",
61
+ ip,
62
+ _BAN_SECONDS,
63
+ _MAX_FAILURES,
64
+ )
65
+
66
+
67
+ def _clear_failures(ip: str) -> None:
68
+ """Clear failure history on successful auth."""
69
+ _failures.pop(ip, None)
70
+
71
+
72
+ def verify_token(request: Request) -> None:
73
+ """Verify the API token from header or query param.
74
+
75
+ Checks ``Authorization: Bearer <token>`` header first, then falls
76
+ back to ``?token=<token>`` query parameter.
77
+
78
+ Raises:
79
+ HTTPException: 403 if IP is banned, 401 if token is invalid,
80
+ 403 if token is missing.
81
+
82
+ If ``VEILRENDER_API_TOKEN`` is not configured, auth is disabled.
83
+ """
84
+ expected = settings.api_token
85
+ if expected is None:
86
+ return
87
+
88
+ ip = _get_client_ip(request)
89
+
90
+ # Check ban first
91
+ if _is_banned(ip):
92
+ raise HTTPException(403, "Temporarily banned due to repeated auth failures")
93
+
94
+ # Check Authorization header
95
+ auth_header = request.headers.get("authorization", "")
96
+ if auth_header.startswith("Bearer "):
97
+ token = auth_header[7:].strip()
98
+ if token == expected:
99
+ _clear_failures(ip)
100
+ return
101
+ _record_failure(ip)
102
+ raise HTTPException(401, "Invalid token")
103
+
104
+ # Check query param
105
+ token_params = request.query_params.get("token", [])
106
+ if token_params:
107
+ if token_params[0] == expected:
108
+ _clear_failures(ip)
109
+ return
110
+ _record_failure(ip)
111
+ raise HTTPException(401, "Invalid token")
112
+
113
+ _record_failure(ip)
114
+ raise HTTPException(403, "Authentication required")
@@ -12,6 +12,7 @@ from cloakbrowser import ensure_binary, get_default_stealth_args
12
12
  from playwright.async_api import Browser, BrowserContext, Page, async_playwright
13
13
 
14
14
  from veilrender.config import settings
15
+ from veilrender.filters import load_blocklist, make_route_handler
15
16
 
16
17
  logger = logging.getLogger(__name__)
17
18
 
@@ -34,6 +35,14 @@ class BrowserManager:
34
35
  self._semaphore = asyncio.Semaphore(settings.max_concurrent)
35
36
  self._lock = asyncio.Lock()
36
37
 
38
+ # Load blocklist once at init if resource filtering is enabled
39
+ if settings.resource_filter:
40
+ self._blocklist = load_blocklist(settings.blocked_domains_extra)
41
+ self._route_handler = make_route_handler(self._blocklist)
42
+ else:
43
+ self._blocklist = frozenset()
44
+ self._route_handler = None
45
+
37
46
  async def start(self) -> None:
38
47
  """Launch Chromium with CDP and connect Playwright to it."""
39
48
  executable_path = ensure_binary()
@@ -173,6 +182,8 @@ class BrowserManager:
173
182
  user_agent=None, # use Playwright default
174
183
  )
175
184
  page = await context.new_page()
185
+ if self._route_handler:
186
+ await page.route("**/*", self._route_handler)
176
187
  yield context, page
177
188
  finally:
178
189
  if context:
@@ -0,0 +1,51 @@
1
+ """Application settings loaded from environment variables."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+
8
+ class Settings:
9
+ """Configuration from ``VEILRENDER_*`` environment variables."""
10
+
11
+ def __init__(self) -> None:
12
+ self.api_token: str | None = os.environ.get("VEILRENDER_API_TOKEN")
13
+ self.port: int = int(os.environ.get("VEILRENDER_PORT", "7860"))
14
+ self.host: str = os.environ.get("VEILRENDER_HOST", "0.0.0.0")
15
+ self.timeout: int = int(os.environ.get("VEILRENDER_TIMEOUT", "30000"))
16
+ self.viewport_width: int = int(
17
+ os.environ.get("VEILRENDER_VIEWPORT_WIDTH", "1280")
18
+ )
19
+ self.viewport_height: int = int(
20
+ os.environ.get("VEILRENDER_VIEWPORT_HEIGHT", "720")
21
+ )
22
+ self.max_concurrent: int = int(os.environ.get("VEILRENDER_MAX_CONCURRENT", "5"))
23
+ self.resource_filter: bool = (
24
+ os.environ.get("VEILRENDER_RESOURCE_FILTER", "true").lower() == "true"
25
+ )
26
+ _extra = os.environ.get("VEILRENDER_BLOCKED_DOMAINS_EXTRA", "")
27
+ self.blocked_domains_extra: list[str] = (
28
+ [d.strip() for d in _extra.split(",") if d.strip()] if _extra else []
29
+ )
30
+
31
+ # Cache settings
32
+ self.cache_enabled: bool = (
33
+ os.environ.get("VEILRENDER_CACHE_ENABLED", "false").lower() == "true"
34
+ )
35
+ self.cache_ttl: int = int(os.environ.get("VEILRENDER_CACHE_TTL", "86400"))
36
+ self.cache_l1_maxsize: int = int(
37
+ os.environ.get("VEILRENDER_CACHE_L1_MAXSIZE", "100")
38
+ )
39
+
40
+ # S3-compatible storage (Cloudflare R2, Oracle Object Storage, etc.)
41
+ self.s3_endpoint: str | None = os.environ.get("VEILRENDER_S3_ENDPOINT")
42
+ self.s3_access_key: str | None = os.environ.get("VEILRENDER_S3_ACCESS_KEY")
43
+ self.s3_secret_key: str | None = os.environ.get("VEILRENDER_S3_SECRET_KEY")
44
+ self.s3_bucket: str = os.environ.get("VEILRENDER_S3_BUCKET", "veilrender-cache")
45
+ self.s3_region: str = os.environ.get("VEILRENDER_S3_REGION", "auto")
46
+ self.s3_secure: bool = (
47
+ os.environ.get("VEILRENDER_S3_SECURE", "true").lower() == "true"
48
+ )
49
+
50
+
51
+ settings = Settings()
File without changes