klyrek-http 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,8 @@
1
+ """klyrek-http: scope-checked, rate-limited HTTP client for the Klyrek ecosystem."""
2
+
3
+ from klyrek_http.client import KlyrekHTTPClient
4
+ from klyrek_http.rate_limit import RateLimiter
5
+
6
+ __version__ = "0.1.0"
7
+
8
+ __all__ = ["KlyrekHTTPClient", "RateLimiter", "__version__"]
klyrek_http/client.py ADDED
@@ -0,0 +1,86 @@
1
+ """A scope-checked, rate-limited HTTP client shared across the Klyrek ecosystem."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from types import TracebackType
6
+ from typing import Any
7
+
8
+ import httpx
9
+
10
+ from klyrek_core.config import KlyrekConfig
11
+ from klyrek_core.logging import get_logger
12
+ from klyrek_core.scope import AuthorizationScope
13
+ from klyrek_http.rate_limit import RateLimiter
14
+
15
+ logger = get_logger("http.client")
16
+
17
+
18
+ class KlyrekHTTPClient:
19
+ """Every Klyrek package that talks to a target should issue requests through this client.
20
+
21
+ It enforces the scan's :class:`AuthorizationScope` and per-host rate limit on every
22
+ request, so no individual package has to remember to do so itself.
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ scope: AuthorizationScope,
28
+ config: KlyrekConfig | None = None,
29
+ transport: httpx.BaseTransport | None = None,
30
+ ) -> None:
31
+ self.scope = scope
32
+ self.config = config or KlyrekConfig()
33
+ min_interval = (
34
+ 1.0 / self.config.rate_limit_per_host if self.config.rate_limit_per_host > 0 else 0.0
35
+ )
36
+ self.rate_limiter = RateLimiter(min_interval=min_interval)
37
+ self._client = httpx.Client(
38
+ headers={"User-Agent": self.config.user_agent},
39
+ timeout=self.config.timeout_seconds,
40
+ verify=self.config.verify_tls,
41
+ follow_redirects=True,
42
+ transport=transport,
43
+ event_hooks={"request": [self._enforce_scope_and_rate_limit]},
44
+ )
45
+
46
+ def _enforce_scope_and_rate_limit(self, request: httpx.Request) -> None:
47
+ # Registered as an httpx "request" hook rather than checked once up front, because
48
+ # follow_redirects=True makes httpx dispatch redirect hops as their own requests
49
+ # internally. A check only in request() would see the original URL and miss a
50
+ # redirect to an out-of-scope host.
51
+ url = str(request.url)
52
+ self.scope.check(url)
53
+ self.rate_limiter.wait(request.url.host)
54
+
55
+ def request(self, method: str, url: str, **kwargs: Any) -> httpx.Response:
56
+ logger.debug("%s %s", method, url)
57
+ return self._client.request(method, url, **kwargs)
58
+
59
+ def get(self, url: str, **kwargs: Any) -> httpx.Response:
60
+ return self.request("GET", url, **kwargs)
61
+
62
+ def post(self, url: str, **kwargs: Any) -> httpx.Response:
63
+ return self.request("POST", url, **kwargs)
64
+
65
+ def put(self, url: str, **kwargs: Any) -> httpx.Response:
66
+ return self.request("PUT", url, **kwargs)
67
+
68
+ def head(self, url: str, **kwargs: Any) -> httpx.Response:
69
+ return self.request("HEAD", url, **kwargs)
70
+
71
+ def delete(self, url: str, **kwargs: Any) -> httpx.Response:
72
+ return self.request("DELETE", url, **kwargs)
73
+
74
+ def close(self) -> None:
75
+ self._client.close()
76
+
77
+ def __enter__(self) -> KlyrekHTTPClient:
78
+ return self
79
+
80
+ def __exit__(
81
+ self,
82
+ exc_type: type[BaseException] | None,
83
+ exc: BaseException | None,
84
+ tb: TracebackType | None,
85
+ ) -> None:
86
+ self.close()
klyrek_http/py.typed ADDED
File without changes
@@ -0,0 +1,37 @@
1
+ """Per-host rate limiting so a scan never exceeds its authorized request rate."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from collections.abc import Callable
7
+ from dataclasses import dataclass, field
8
+
9
+
10
+ @dataclass
11
+ class RateLimiter:
12
+ """Enforces a minimum interval between requests to the same host.
13
+
14
+ Hosts are tracked independently: waiting for ``a.com`` never blocks a
15
+ concurrent request to ``b.com``. ``clock``/``sleep`` are injectable so
16
+ tests can run instantly against a fake clock.
17
+ """
18
+
19
+ min_interval: float = 0.0
20
+ clock: Callable[[], float] = time.monotonic
21
+ sleep: Callable[[float], None] = time.sleep
22
+ _last_request_at: dict[str, float] = field(default_factory=dict, repr=False)
23
+
24
+ def wait(self, host: str) -> None:
25
+ """Block, if needed, until ``min_interval`` has passed since the last request to host."""
26
+ if self.min_interval <= 0:
27
+ self._last_request_at[host] = self.clock()
28
+ return
29
+
30
+ now = self.clock()
31
+ last = self._last_request_at.get(host)
32
+ if last is not None:
33
+ remaining = self.min_interval - (now - last)
34
+ if remaining > 0:
35
+ self.sleep(remaining)
36
+ now = self.clock()
37
+ self._last_request_at[host] = now
@@ -0,0 +1,45 @@
1
+ Metadata-Version: 2.4
2
+ Name: klyrek-http
3
+ Version: 0.1.0
4
+ Summary: HTTP client abstraction and request management for the Klyrek ecosystem
5
+ Author: Klyrek Contributors
6
+ License: MIT
7
+ Keywords: appsec,http,pentesting,reconnaissance,security
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Information Technology
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Security
13
+ Requires-Python: >=3.10
14
+ Requires-Dist: httpx>=0.27
15
+ Requires-Dist: klyrek-core
16
+ Provides-Extra: dev
17
+ Requires-Dist: mypy>=1.10; extra == 'dev'
18
+ Requires-Dist: pytest>=8.0; extra == 'dev'
19
+ Requires-Dist: ruff>=0.6; extra == 'dev'
20
+ Description-Content-Type: text/markdown
21
+
22
+ # klyrek-http
23
+
24
+ The HTTP layer shared by every Klyrek package that talks to a target: `klyrek-crawler`,
25
+ `klyrek-api`, `klyrek-tech`, `klyrek-auth`, `klyrek-js`, `klyrek-assets`, and `klyrek-headers` all
26
+ issue requests through `KlyrekHTTPClient` rather than calling `httpx`/`requests` directly.
27
+
28
+ Every request goes through two checks before it leaves the process:
29
+
30
+ 1. **Scope enforcement** — the URL's host must be declared in the scan's
31
+ [`AuthorizationScope`](../klyrek-core/README.md#scope-enforcement), or the request raises
32
+ `ScopeViolationError` instead of firing.
33
+ 2. **Rate limiting** — a per-host minimum interval (`KlyrekConfig.rate_limit_per_host`) keeps a
34
+ scan from hammering a target faster than was authorized.
35
+
36
+ ```python
37
+ from klyrek_core.config import KlyrekConfig
38
+ from klyrek_core.scope import AuthorizationScope
39
+ from klyrek_http.client import KlyrekHTTPClient
40
+
41
+ scope = AuthorizationScope(authorized_hosts=["target.com", "*.target.com"])
42
+
43
+ with KlyrekHTTPClient(scope, config=KlyrekConfig(rate_limit_per_host=5)) as client:
44
+ response = client.get("https://target.com/")
45
+ ```
@@ -0,0 +1,7 @@
1
+ klyrek_http/__init__.py,sha256=zfdB3REfyozkgkVr4JP8zzrkeAnHJDoeAo8zwGqcyQ0,266
2
+ klyrek_http/client.py,sha256=n45IFg6HGrEphVNJBHQBFjru4V4ghnbcTlfhPYWhI94,3059
3
+ klyrek_http/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ klyrek_http/rate_limit.py,sha256=wQ7Uy5ZpKu8qpN_Fjmu06Qz66ynTYiqVl6WRmyIVN0c,1280
5
+ klyrek_http-0.1.0.dist-info/METADATA,sha256=OOcc-7xwHWFu9VBH-ERSR78n-F6GoPUrc00T3ZhomTY,1828
6
+ klyrek_http-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
7
+ klyrek_http-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any