unique-search-proxy-core 2026.42.0.dev0__tar.gz → 2026.42.0.dev2__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.
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/PKG-INFO +2 -2
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/pyproject.toml +2 -2
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/http_client/client.py +52 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/url_safety/__init__.py +5 -1
- unique_search_proxy_core-2026.42.0.dev2/unique_search_proxy_core/url_safety/egress.py +96 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/url_safety/redirect.py +15 -6
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/url_safety/settings.py +10 -6
- unique_search_proxy_core-2026.42.0.dev0/unique_search_proxy_core/url_safety/egress.py +0 -21
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/README.md +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/__init__.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/agent_engines/__init__.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/agent_engines/base.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/agent_engines/bing/__init__.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/agent_engines/bing/enums.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/agent_engines/bing/grounding.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/agent_engines/bing/schema.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/agent_engines/bing/settings.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/agent_engines/config_types.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/agent_engines/output_schema.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/agent_engines/resolve.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/agent_engines/response_parsing.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/agent_engines/vertexai/__init__.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/agent_engines/vertexai/schema.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/agent_engines/vertexai/settings.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/context.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/crawlers/__init__.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/crawlers/base.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/crawlers/basic/__init__.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/crawlers/basic/content_types.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/crawlers/basic/processing/policy.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/crawlers/basic/schema.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/crawlers/config_types.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/crawlers/firecrawl/schema.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/crawlers/jina/schema.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/crawlers/tavily/schema.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/errors.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/http_client/__init__.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/http_client/credentials.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/http_client/secrets.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/http_client/settings.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/logging.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/param_policy/__init__.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/param_policy/annotations.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/param_policy/derive.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/param_policy/exposable_config.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/param_policy/exposable_param.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/param_policy/exposed_params.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/param_policy/request_base.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/providers/schema.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/schema.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/search_engines/__init__.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/search_engines/base.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/search_engines/brave/__init__.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/search_engines/brave/schema.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/search_engines/config_types.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/search_engines/google/__init__.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/search_engines/google/schema.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/search_engines/perplexity/__init__.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/search_engines/perplexity/schema.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/url_safety/dns.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/url_safety/models.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/url_safety/netloc.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/url_safety/policy.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/url_safety/resolver.py +0 -0
- {unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/unique_search_proxy_core/url_safety/service.py +0 -0
{unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: unique-search-proxy-core
|
|
3
|
-
Version: 2026.42.0.
|
|
3
|
+
Version: 2026.42.0.dev2
|
|
4
4
|
Summary: Shared Pydantic types for the Unique Search Proxy API
|
|
5
5
|
Author: ThePhilAz
|
|
6
6
|
Author-email: ThePhilAz <rami.azouz@philico.com>
|
|
@@ -8,7 +8,7 @@ Requires-Dist: httpx>=0.28.0,<0.29
|
|
|
8
8
|
Requires-Dist: pydantic>=2.12.5,<3.0.0
|
|
9
9
|
Requires-Dist: pydantic-settings>=2.10.1,<3
|
|
10
10
|
Requires-Dist: pyhumps>=3.8.0,<4
|
|
11
|
-
Requires-Dist: unique-toolkit[monitoring]>=2026.42.0.
|
|
11
|
+
Requires-Dist: unique-toolkit[monitoring]>=2026.42.0.dev13,<2026.42.0rc0
|
|
12
12
|
Requires-Python: >=3.12
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
|
{unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/pyproject.toml
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "unique-search-proxy-core"
|
|
3
|
-
version = "2026.42.0.
|
|
3
|
+
version = "2026.42.0.dev2"
|
|
4
4
|
description = "Shared Pydantic types for the Unique Search Proxy API"
|
|
5
5
|
authors = [{ name = "ThePhilAz", email = "rami.azouz@philico.com" }]
|
|
6
6
|
readme = "README.md"
|
|
@@ -10,7 +10,7 @@ dependencies = [
|
|
|
10
10
|
"pydantic>=2.12.5,<3.0.0",
|
|
11
11
|
"pydantic-settings>=2.10.1,<3",
|
|
12
12
|
"pyhumps>=3.8.0,<4",
|
|
13
|
-
"unique-toolkit[monitoring]>=2026.42.0.
|
|
13
|
+
"unique-toolkit[monitoring]>=2026.42.0.dev13,<2026.42.0rc0",
|
|
14
14
|
|
|
15
15
|
]
|
|
16
16
|
|
|
@@ -25,8 +25,13 @@ from unique_search_proxy_core.http_client.settings import (
|
|
|
25
25
|
ProxyAuthMode,
|
|
26
26
|
ProxySettings,
|
|
27
27
|
)
|
|
28
|
+
from unique_search_proxy_core.url_safety.settings import url_safety_settings
|
|
28
29
|
|
|
29
30
|
_LOGGER = logging.getLogger(__name__)
|
|
31
|
+
_PROXY_REMEDIATION = (
|
|
32
|
+
"Either set URL_SAFETY_ENABLED=true to enable application URL-safety checks, "
|
|
33
|
+
"or configure an authenticated corporate proxy for all egress."
|
|
34
|
+
)
|
|
30
35
|
|
|
31
36
|
|
|
32
37
|
@dataclass(frozen=True)
|
|
@@ -207,6 +212,51 @@ def async_client_factory(
|
|
|
207
212
|
)
|
|
208
213
|
|
|
209
214
|
|
|
215
|
+
def _validate_url_safety_proxy_configuration(proxy: ProxySettings) -> None:
|
|
216
|
+
"""Reject unsafe egress before the registry can return an HTTP client."""
|
|
217
|
+
if url_safety_settings.enabled:
|
|
218
|
+
return
|
|
219
|
+
|
|
220
|
+
if (
|
|
221
|
+
not proxy.proxy_host
|
|
222
|
+
or proxy.proxy_port is None
|
|
223
|
+
or not 1 <= proxy.proxy_port <= 65535
|
|
224
|
+
):
|
|
225
|
+
raise ValueError(
|
|
226
|
+
"URL_SAFETY_ENABLED=false disables application URL-safety checks, "
|
|
227
|
+
"but no usable corporate proxy endpoint is configured: proxy_host "
|
|
228
|
+
"must be non-empty and proxy_port must be between 1 and 65535. "
|
|
229
|
+
+ _PROXY_REMEDIATION,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
uses_authenticated_proxy = proxy.proxy_auth_mode != "none"
|
|
233
|
+
uses_proxy_for_every_user = (
|
|
234
|
+
proxy.proxy_username_source == "user_metadata"
|
|
235
|
+
and not proxy.per_user_proxy_company_ids
|
|
236
|
+
)
|
|
237
|
+
if not uses_authenticated_proxy and not uses_proxy_for_every_user:
|
|
238
|
+
raise ValueError(
|
|
239
|
+
"URL_SAFETY_ENABLED=false disables application URL-safety checks, "
|
|
240
|
+
"but the current proxy settings do not route every request through "
|
|
241
|
+
"an authenticated corporate proxy. Direct or partially proxied "
|
|
242
|
+
"egress is incompatible. " + _PROXY_REMEDIATION,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
if proxy.proxy_auth_mode == "ssl_tls" and proxy.proxy_ssl_cert_path is None:
|
|
246
|
+
raise ValueError(
|
|
247
|
+
"URL_SAFETY_ENABLED=false disables application URL-safety checks, "
|
|
248
|
+
"but SSL/TLS proxy authentication is missing proxy_ssl_cert_path. "
|
|
249
|
+
+ _PROXY_REMEDIATION,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
_LOGGER.warning(
|
|
253
|
+
"Application URL safety checks are disabled because "
|
|
254
|
+
"URL_SAFETY_ENABLED=false. All egress must use a configured corporate "
|
|
255
|
+
"proxy. Note: the corporate proxy URL-safety policy is not verified by "
|
|
256
|
+
"the application."
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
210
260
|
class HttpClientRegistry:
|
|
211
261
|
"""Bounded LRU of httpx clients keyed by proxy credentials.
|
|
212
262
|
|
|
@@ -223,6 +273,8 @@ class HttpClientRegistry:
|
|
|
223
273
|
*,
|
|
224
274
|
fixed_client: AsyncClient | None = None,
|
|
225
275
|
) -> None:
|
|
276
|
+
if fixed_client is None:
|
|
277
|
+
_validate_url_safety_proxy_configuration(settings)
|
|
226
278
|
self._settings = settings
|
|
227
279
|
self._resolver = resolver
|
|
228
280
|
self._fixed_client = fixed_client
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
from unique_search_proxy_core.url_safety.egress import
|
|
1
|
+
from unique_search_proxy_core.url_safety.egress import (
|
|
2
|
+
pinned_httpx_get_args,
|
|
3
|
+
safe_pinned_httpx_get,
|
|
4
|
+
)
|
|
2
5
|
from unique_search_proxy_core.url_safety.models import (
|
|
3
6
|
BlockedCrawlTarget,
|
|
4
7
|
CrawlTargetValidationError,
|
|
@@ -21,5 +24,6 @@ __all__ = [
|
|
|
21
24
|
"UrlSafetySettings",
|
|
22
25
|
"bypass_crawl_target",
|
|
23
26
|
"pinned_httpx_get_args",
|
|
27
|
+
"safe_pinned_httpx_get",
|
|
24
28
|
"url_safety_settings",
|
|
25
29
|
]
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from urllib.parse import urlsplit
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from unique_search_proxy_core.url_safety.models import (
|
|
9
|
+
BlockedCrawlTarget,
|
|
10
|
+
CrawlTargetValidationError,
|
|
11
|
+
ResolvedCrawlTarget,
|
|
12
|
+
bypass_crawl_target,
|
|
13
|
+
)
|
|
14
|
+
from unique_search_proxy_core.url_safety.redirect import redirect_target_url
|
|
15
|
+
from unique_search_proxy_core.url_safety.resolver import resolve_crawl_target
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def pinned_httpx_get_args(
|
|
19
|
+
target: ResolvedCrawlTarget,
|
|
20
|
+
) -> tuple[str, dict[str, str], dict[str, str]]:
|
|
21
|
+
"""Build httpx GET url, headers, and extensions for DNS-pinned egress."""
|
|
22
|
+
headers: dict[str, str] = {}
|
|
23
|
+
if target.host_header is not None:
|
|
24
|
+
headers["Host"] = target.host_header
|
|
25
|
+
|
|
26
|
+
extensions: dict[str, str] = {}
|
|
27
|
+
if target.sni_hostname is not None:
|
|
28
|
+
extensions["sni_hostname"] = target.sni_hostname
|
|
29
|
+
|
|
30
|
+
return target.request_url, headers, extensions
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def safe_pinned_httpx_get(
|
|
34
|
+
client: httpx.AsyncClient,
|
|
35
|
+
target: ResolvedCrawlTarget,
|
|
36
|
+
*,
|
|
37
|
+
headers: Mapping[str, str] | None = None,
|
|
38
|
+
timeout: float | httpx.Timeout,
|
|
39
|
+
max_redirect_hops: int,
|
|
40
|
+
enforce_url_safety: bool,
|
|
41
|
+
) -> httpx.Response:
|
|
42
|
+
"""GET a target, applying the original target's safety mode to redirects."""
|
|
43
|
+
if max_redirect_hops < 0:
|
|
44
|
+
raise ValueError("max_redirect_hops must be non-negative")
|
|
45
|
+
|
|
46
|
+
current_target = target
|
|
47
|
+
for redirects_followed in range(max_redirect_hops + 1):
|
|
48
|
+
request_url, pin_headers, extensions = pinned_httpx_get_args(current_target)
|
|
49
|
+
response = await client.get(
|
|
50
|
+
request_url,
|
|
51
|
+
headers={**(headers or {}), **pin_headers},
|
|
52
|
+
extensions=extensions or None,
|
|
53
|
+
timeout=timeout,
|
|
54
|
+
follow_redirects=False,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
next_url = redirect_target_url(response, current_target.normalized_url)
|
|
59
|
+
if next_url is not None:
|
|
60
|
+
parsed_next_url = urlsplit(next_url)
|
|
61
|
+
_ = parsed_next_url.hostname
|
|
62
|
+
_ = parsed_next_url.port
|
|
63
|
+
except ValueError as exc:
|
|
64
|
+
raise CrawlTargetValidationError(
|
|
65
|
+
[
|
|
66
|
+
BlockedCrawlTarget(
|
|
67
|
+
hostname=current_target.hostname or None,
|
|
68
|
+
category="redirect",
|
|
69
|
+
reason="Redirect target URL is missing or malformed",
|
|
70
|
+
)
|
|
71
|
+
]
|
|
72
|
+
) from exc
|
|
73
|
+
if next_url is None:
|
|
74
|
+
return response
|
|
75
|
+
|
|
76
|
+
if redirects_followed == max_redirect_hops:
|
|
77
|
+
raise CrawlTargetValidationError(
|
|
78
|
+
[
|
|
79
|
+
BlockedCrawlTarget(
|
|
80
|
+
hostname=current_target.hostname or None,
|
|
81
|
+
category="redirect",
|
|
82
|
+
reason="Maximum redirect hop count exceeded",
|
|
83
|
+
)
|
|
84
|
+
]
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
current_target = (
|
|
88
|
+
await resolve_crawl_target(next_url)
|
|
89
|
+
if enforce_url_safety
|
|
90
|
+
else bypass_crawl_target(next_url)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
raise AssertionError("redirect loop must return or raise")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
__all__ = ["pinned_httpx_get_args", "safe_pinned_httpx_get"]
|
|
@@ -28,6 +28,18 @@ _REDIRECT_PROBE_USER_AGENT = (
|
|
|
28
28
|
ValidateUrlFn = Callable[[str], Awaitable[tuple[str, str] | None]]
|
|
29
29
|
|
|
30
30
|
|
|
31
|
+
def redirect_target_url(response: httpx.Response, current_url: str) -> str | None:
|
|
32
|
+
"""Return the next absolute redirect URL, or ``None`` when not followable."""
|
|
33
|
+
if response.status_code not in _REDIRECT_STATUS_CODES:
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
location = response.headers.get("location")
|
|
37
|
+
if not location:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
return urljoin(current_url, location)
|
|
41
|
+
|
|
42
|
+
|
|
31
43
|
@asynccontextmanager
|
|
32
44
|
async def _redirect_probe_client(
|
|
33
45
|
http_client: httpx.AsyncClient | None,
|
|
@@ -105,14 +117,11 @@ async def resolve_redirect_chain(
|
|
|
105
117
|
]
|
|
106
118
|
) from exc
|
|
107
119
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
location = resp.headers.get("location")
|
|
112
|
-
if not location:
|
|
120
|
+
redirect_url = redirect_target_url(resp, current)
|
|
121
|
+
if redirect_url is None:
|
|
113
122
|
break
|
|
114
123
|
|
|
115
|
-
current =
|
|
124
|
+
current = redirect_url
|
|
116
125
|
|
|
117
126
|
error = await validate_url(current)
|
|
118
127
|
if error is not None:
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from
|
|
4
|
-
|
|
3
|
+
from pydantic import Field
|
|
5
4
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
6
5
|
|
|
7
|
-
_LOGGER = getLogger(__name__)
|
|
8
|
-
|
|
9
6
|
|
|
10
7
|
class UrlSafetySettings(BaseSettings):
|
|
11
|
-
enabled: bool =
|
|
8
|
+
enabled: bool = Field(
|
|
9
|
+
default=True,
|
|
10
|
+
description=(
|
|
11
|
+
"Whether the application enforces URL-safety checks. Setting this to "
|
|
12
|
+
"false bypasses application checks and requires every request to use "
|
|
13
|
+
"a configured corporate proxy; the application does not verify the "
|
|
14
|
+
"proxy's URL-safety policy."
|
|
15
|
+
),
|
|
16
|
+
)
|
|
12
17
|
resolve_redirects: bool = True
|
|
13
18
|
allowed_schemes: list[str] = ["http", "https"]
|
|
14
19
|
localhost_hosts: list[str] = [
|
|
@@ -36,4 +41,3 @@ class UrlSafetySettings(BaseSettings):
|
|
|
36
41
|
|
|
37
42
|
|
|
38
43
|
url_safety_settings = UrlSafetySettings()
|
|
39
|
-
_LOGGER.info("URL Safety is enabled: %s", url_safety_settings.enabled)
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from unique_search_proxy_core.url_safety.models import ResolvedCrawlTarget
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def pinned_httpx_get_args(
|
|
7
|
-
target: ResolvedCrawlTarget,
|
|
8
|
-
) -> tuple[str, dict[str, str], dict[str, str]]:
|
|
9
|
-
"""Build httpx GET url, headers, and extensions for DNS-pinned egress."""
|
|
10
|
-
headers: dict[str, str] = {}
|
|
11
|
-
if target.host_header is not None:
|
|
12
|
-
headers["Host"] = target.host_header
|
|
13
|
-
|
|
14
|
-
extensions: dict[str, str] = {}
|
|
15
|
-
if target.sni_hostname is not None:
|
|
16
|
-
extensions["sni_hostname"] = target.sni_hostname
|
|
17
|
-
|
|
18
|
-
return target.request_url, headers, extensions
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
__all__ = ["pinned_httpx_get_args"]
|
{unique_search_proxy_core-2026.42.0.dev0 → unique_search_proxy_core-2026.42.0.dev2}/README.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|