coco-framework 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.
- coco/__init__.py +3 -0
- coco/agents/__init__.py +9 -0
- coco/agents/_subagent_transformer.py +307 -0
- coco/agents/factory.py +2164 -0
- coco/agents/middleware/__init__.py +100 -0
- coco/agents/middleware/_execution.py +385 -0
- coco/agents/middleware/_redaction.py +454 -0
- coco/agents/middleware/_retry.py +134 -0
- coco/agents/middleware/_trace_policy.py +61 -0
- coco/agents/middleware/context_editing.py +306 -0
- coco/agents/middleware/file_search.py +433 -0
- coco/agents/middleware/human_in_the_loop.py +680 -0
- coco/agents/middleware/internal_call_transformer.py +108 -0
- coco/agents/middleware/model_call_limit.py +267 -0
- coco/agents/middleware/model_fallback.py +417 -0
- coco/agents/middleware/model_retry.py +327 -0
- coco/agents/middleware/pii.py +878 -0
- coco/agents/middleware/provider_tool_search.py +307 -0
- coco/agents/middleware/shell_tool.py +949 -0
- coco/agents/middleware/summarization.py +910 -0
- coco/agents/middleware/todo.py +357 -0
- coco/agents/middleware/tool_call_limit.py +495 -0
- coco/agents/middleware/tool_emulator.py +251 -0
- coco/agents/middleware/tool_error.py +209 -0
- coco/agents/middleware/tool_retry.py +421 -0
- coco/agents/middleware/tool_selection.py +515 -0
- coco/agents/middleware/types.py +2201 -0
- coco/agents/structured_output.py +472 -0
- coco/chat_models/__init__.py +7 -0
- coco/chat_models/base.py +1082 -0
- coco/embeddings/__init__.py +18 -0
- coco/embeddings/base.py +279 -0
- coco/mcp/__init__.py +36 -0
- coco/mcp/adapter.py +245 -0
- coco/mcp/elicitation.py +394 -0
- coco/mcp/tools.py +307 -0
- coco/messages/__init__.py +73 -0
- coco/py.typed +0 -0
- coco/rate_limiters/__init__.py +13 -0
- coco/tools/__init__.py +22 -0
- coco/tools/tool_node.py +20 -0
- coco_anthropic/__init__.py +15 -0
- coco_anthropic/_client_utils.py +81 -0
- coco_anthropic/_compat.py +228 -0
- coco_anthropic/_sdk_compat.py +124 -0
- coco_anthropic/_version.py +3 -0
- coco_anthropic/chat_models.py +3349 -0
- coco_anthropic/data/__init__.py +1 -0
- coco_anthropic/data/_profiles.py +491 -0
- coco_anthropic/data/profile_augmentations.toml +69 -0
- coco_anthropic/experimental.py +140 -0
- coco_anthropic/llms.py +428 -0
- coco_anthropic/middleware/__init__.py +25 -0
- coco_anthropic/middleware/anthropic_tools.py +1197 -0
- coco_anthropic/middleware/bash.py +84 -0
- coco_anthropic/middleware/file_search.py +360 -0
- coco_anthropic/middleware/prompt_caching.py +276 -0
- coco_anthropic/output_parsers.py +93 -0
- coco_anthropic/py.typed +0 -0
- coco_core/__init__.py +20 -0
- coco_core/_api/__init__.py +87 -0
- coco_core/_api/beta_decorator.py +266 -0
- coco_core/_api/deprecation.py +637 -0
- coco_core/_api/internal.py +23 -0
- coco_core/_api/path.py +50 -0
- coco_core/_import_utils.py +41 -0
- coco_core/_protocol.py +625 -0
- coco_core/_security/__init__.py +36 -0
- coco_core/_security/_exceptions.py +9 -0
- coco_core/_security/_policy.py +313 -0
- coco_core/_security/_ssrf_protection.py +155 -0
- coco_core/_security/_transport.py +254 -0
- coco_core/agents.py +258 -0
- coco_core/caches.py +272 -0
- coco_core/callbacks/__init__.py +132 -0
- coco_core/callbacks/base.py +1229 -0
- coco_core/callbacks/file.py +266 -0
- coco_core/callbacks/manager.py +2826 -0
- coco_core/callbacks/stdout.py +123 -0
- coco_core/callbacks/streaming_stdout.py +154 -0
- coco_core/callbacks/usage.py +124 -0
- coco_core/chat_history.py +267 -0
- coco_core/chat_loaders.py +26 -0
- coco_core/chat_sessions.py +19 -0
- coco_core/cross_encoders.py +18 -0
- coco_core/document_loaders/__init__.py +36 -0
- coco_core/document_loaders/base.py +155 -0
- coco_core/document_loaders/blob_loaders.py +38 -0
- coco_core/documents/__init__.py +55 -0
- coco_core/documents/base.py +347 -0
- coco_core/documents/compressor.py +74 -0
- coco_core/documents/transformers.py +79 -0
- coco_core/embeddings/__init__.py +31 -0
- coco_core/embeddings/embeddings.py +78 -0
- coco_core/embeddings/fake.py +129 -0
- coco_core/env.py +22 -0
- coco_core/example_selectors/__init__.py +47 -0
- coco_core/example_selectors/base.py +60 -0
- coco_core/example_selectors/length_based.py +131 -0
- coco_core/example_selectors/semantic_similarity.py +366 -0
- coco_core/exceptions.py +166 -0
- coco_core/globals.py +72 -0
- coco_core/indexing/__init__.py +53 -0
- coco_core/indexing/api.py +954 -0
- coco_core/indexing/base.py +661 -0
- coco_core/indexing/in_memory.py +104 -0
- coco_core/language_models/__init__.py +116 -0
- coco_core/language_models/_compat_bridge.py +844 -0
- coco_core/language_models/_utils.py +343 -0
- coco_core/language_models/base.py +500 -0
- coco_core/language_models/chat_model_stream.py +1525 -0
- coco_core/language_models/chat_models.py +2748 -0
- coco_core/language_models/fake.py +137 -0
- coco_core/language_models/fake_chat_models.py +396 -0
- coco_core/language_models/llms.py +1569 -0
- coco_core/language_models/model_profile.py +183 -0
- coco_core/load/__init__.py +44 -0
- coco_core/load/_validation.py +191 -0
- coco_core/load/dump.py +120 -0
- coco_core/load/load.py +828 -0
- coco_core/load/mapping.py +1097 -0
- coco_core/load/serializable.py +406 -0
- coco_core/messages/__init__.py +198 -0
- coco_core/messages/ai.py +854 -0
- coco_core/messages/base.py +518 -0
- coco_core/messages/block_translators/__init__.py +112 -0
- coco_core/messages/block_translators/anthropic.py +534 -0
- coco_core/messages/block_translators/bedrock.py +108 -0
- coco_core/messages/block_translators/bedrock_converse.py +329 -0
- coco_core/messages/block_translators/coco_v0.py +303 -0
- coco_core/messages/block_translators/google_genai.py +610 -0
- coco_core/messages/block_translators/google_vertexai.py +21 -0
- coco_core/messages/block_translators/groq.py +157 -0
- coco_core/messages/block_translators/openai.py +1089 -0
- coco_core/messages/chat.py +64 -0
- coco_core/messages/content.py +1488 -0
- coco_core/messages/function.py +62 -0
- coco_core/messages/human.py +70 -0
- coco_core/messages/modifier.py +33 -0
- coco_core/messages/system.py +70 -0
- coco_core/messages/tool.py +418 -0
- coco_core/messages/utils.py +2442 -0
- coco_core/output_parsers/__init__.py +101 -0
- coco_core/output_parsers/base.py +361 -0
- coco_core/output_parsers/format_instructions.py +16 -0
- coco_core/output_parsers/json.py +139 -0
- coco_core/output_parsers/list.py +249 -0
- coco_core/output_parsers/openai_functions.py +324 -0
- coco_core/output_parsers/openai_tools.py +383 -0
- coco_core/output_parsers/pydantic.py +141 -0
- coco_core/output_parsers/string.py +63 -0
- coco_core/output_parsers/transform.py +175 -0
- coco_core/output_parsers/xml.py +301 -0
- coco_core/outputs/__init__.py +66 -0
- coco_core/outputs/chat_generation.py +147 -0
- coco_core/outputs/chat_result.py +39 -0
- coco_core/outputs/generation.py +80 -0
- coco_core/outputs/llm_result.py +112 -0
- coco_core/outputs/run_info.py +22 -0
- coco_core/prompt_values.py +161 -0
- coco_core/prompts/__init__.py +101 -0
- coco_core/prompts/base.py +508 -0
- coco_core/prompts/chat.py +1495 -0
- coco_core/prompts/dict.py +177 -0
- coco_core/prompts/few_shot.py +483 -0
- coco_core/prompts/few_shot_with_templates.py +239 -0
- coco_core/prompts/image.py +177 -0
- coco_core/prompts/loading.py +296 -0
- coco_core/prompts/message.py +97 -0
- coco_core/prompts/prompt.py +312 -0
- coco_core/prompts/string.py +399 -0
- coco_core/prompts/structured.py +214 -0
- coco_core/py.typed +0 -0
- coco_core/rate_limiters.py +254 -0
- coco_core/retrievers.py +328 -0
- coco_core/runnables/__init__.py +136 -0
- coco_core/runnables/base.py +6725 -0
- coco_core/runnables/branch.py +457 -0
- coco_core/runnables/config.py +712 -0
- coco_core/runnables/configurable.py +710 -0
- coco_core/runnables/fallbacks.py +663 -0
- coco_core/runnables/graph.py +741 -0
- coco_core/runnables/graph_ascii.py +366 -0
- coco_core/runnables/graph_mermaid.py +503 -0
- coco_core/runnables/graph_png.py +215 -0
- coco_core/runnables/history.py +631 -0
- coco_core/runnables/passthrough.py +849 -0
- coco_core/runnables/retry.py +379 -0
- coco_core/runnables/router.py +239 -0
- coco_core/runnables/schema.py +188 -0
- coco_core/runnables/utils.py +798 -0
- coco_core/stores.py +291 -0
- coco_core/structured_query.py +203 -0
- coco_core/sys_info.py +154 -0
- coco_core/tools/__init__.py +95 -0
- coco_core/tools/base.py +1948 -0
- coco_core/tools/convert.py +497 -0
- coco_core/tools/render.py +67 -0
- coco_core/tools/retriever.py +94 -0
- coco_core/tools/simple.py +208 -0
- coco_core/tools/structured.py +322 -0
- coco_core/tracers/__init__.py +47 -0
- coco_core/tracers/_compat.py +95 -0
- coco_core/tracers/_context.py +337 -0
- coco_core/tracers/_streaming.py +53 -0
- coco_core/tracers/base.py +955 -0
- coco_core/tracers/coco.py +279 -0
- coco_core/tracers/context.py +205 -0
- coco_core/tracers/core.py +766 -0
- coco_core/tracers/event_stream.py +1101 -0
- coco_core/tracers/log_stream.py +765 -0
- coco_core/tracers/memory_stream.py +200 -0
- coco_core/tracers/root_listeners.py +130 -0
- coco_core/tracers/run_collector.py +40 -0
- coco_core/tracers/schemas.py +14 -0
- coco_core/tracers/stdout.py +205 -0
- coco_core/utils/__init__.py +111 -0
- coco_core/utils/_gateway.py +274 -0
- coco_core/utils/_merge.py +218 -0
- coco_core/utils/aiter.py +357 -0
- coco_core/utils/env.py +86 -0
- coco_core/utils/formatting.py +80 -0
- coco_core/utils/function_calling.py +869 -0
- coco_core/utils/html.py +132 -0
- coco_core/utils/image.py +15 -0
- coco_core/utils/input.py +82 -0
- coco_core/utils/interactive_env.py +12 -0
- coco_core/utils/iter.py +229 -0
- coco_core/utils/json.py +230 -0
- coco_core/utils/json_schema.py +273 -0
- coco_core/utils/mustache.py +706 -0
- coco_core/utils/pydantic.py +631 -0
- coco_core/utils/strings.py +69 -0
- coco_core/utils/usage.py +61 -0
- coco_core/utils/utils.py +528 -0
- coco_core/utils/uuid.py +57 -0
- coco_core/vectorstores/__init__.py +53 -0
- coco_core/vectorstores/base.py +1111 -0
- coco_core/vectorstores/in_memory.py +546 -0
- coco_core/vectorstores/utils.py +163 -0
- coco_core/version.py +3 -0
- coco_framework-0.1.0.dist-info/METADATA +184 -0
- coco_framework-0.1.0.dist-info/RECORD +400 -0
- coco_framework-0.1.0.dist-info/WHEEL +4 -0
- coco_framework-0.1.0.dist-info/licenses/LICENSE +24 -0
- coco_framework-0.1.0.dist-info/licenses/NOTICE +15 -0
- coco_graph/_internal/__init__.py +4 -0
- coco_graph/_internal/_cache.py +31 -0
- coco_graph/_internal/_config.py +474 -0
- coco_graph/_internal/_constants.py +140 -0
- coco_graph/_internal/_fields.py +213 -0
- coco_graph/_internal/_future.py +220 -0
- coco_graph/_internal/_pydantic.py +275 -0
- coco_graph/_internal/_queue.py +124 -0
- coco_graph/_internal/_replay.py +90 -0
- coco_graph/_internal/_retry.py +29 -0
- coco_graph/_internal/_runnable.py +969 -0
- coco_graph/_internal/_scratchpad.py +19 -0
- coco_graph/_internal/_serde.py +253 -0
- coco_graph/_internal/_timeout.py +25 -0
- coco_graph/_internal/_typing.py +54 -0
- coco_graph/cache/base/__init__.py +48 -0
- coco_graph/cache/base/py.typed +0 -0
- coco_graph/cache/memory/__init__.py +73 -0
- coco_graph/cache/redis/__init__.py +144 -0
- coco_graph/cache/sqlite/__init__.py +120 -0
- coco_graph/callbacks.py +394 -0
- coco_graph/channels/__init__.py +29 -0
- coco_graph/channels/any_value.py +72 -0
- coco_graph/channels/base.py +121 -0
- coco_graph/channels/binop.py +155 -0
- coco_graph/channels/delta.py +202 -0
- coco_graph/channels/ephemeral_value.py +79 -0
- coco_graph/channels/last_value.py +151 -0
- coco_graph/channels/named_barrier_value.py +167 -0
- coco_graph/channels/topic.py +94 -0
- coco_graph/channels/untracked_value.py +73 -0
- coco_graph/checkpoint/base/__init__.py +861 -0
- coco_graph/checkpoint/base/id.py +109 -0
- coco_graph/checkpoint/base/py.typed +0 -0
- coco_graph/checkpoint/memory/__init__.py +698 -0
- coco_graph/checkpoint/memory/py.typed +0 -0
- coco_graph/checkpoint/postgres/__init__.py +602 -0
- coco_graph/checkpoint/postgres/_ainternal.py +23 -0
- coco_graph/checkpoint/postgres/_internal.py +21 -0
- coco_graph/checkpoint/postgres/aio.py +691 -0
- coco_graph/checkpoint/postgres/base.py +666 -0
- coco_graph/checkpoint/postgres/py.typed +0 -0
- coco_graph/checkpoint/postgres/shallow.py +967 -0
- coco_graph/checkpoint/serde/__init__.py +0 -0
- coco_graph/checkpoint/serde/_msgpack.py +97 -0
- coco_graph/checkpoint/serde/base.py +64 -0
- coco_graph/checkpoint/serde/encrypted.py +80 -0
- coco_graph/checkpoint/serde/event_hooks.py +52 -0
- coco_graph/checkpoint/serde/jsonplus.py +883 -0
- coco_graph/checkpoint/serde/py.typed +0 -0
- coco_graph/checkpoint/serde/types.py +68 -0
- coco_graph/checkpoint/sqlite/__init__.py +645 -0
- coco_graph/checkpoint/sqlite/_delta.py +172 -0
- coco_graph/checkpoint/sqlite/aio.py +738 -0
- coco_graph/checkpoint/sqlite/py.typed +0 -0
- coco_graph/checkpoint/sqlite/utils.py +116 -0
- coco_graph/config.py +196 -0
- coco_graph/constants.py +64 -0
- coco_graph/errors.py +241 -0
- coco_graph/func/__init__.py +620 -0
- coco_graph/graph/__init__.py +12 -0
- coco_graph/graph/_branch.py +225 -0
- coco_graph/graph/_node.py +103 -0
- coco_graph/graph/message.py +437 -0
- coco_graph/graph/state.py +1978 -0
- coco_graph/graph/ui.py +227 -0
- coco_graph/managed/__init__.py +3 -0
- coco_graph/managed/base.py +31 -0
- coco_graph/managed/is_last_step.py +24 -0
- coco_graph/prebuilt/__init__.py +23 -0
- coco_graph/prebuilt/_tool_call_stream.py +117 -0
- coco_graph/prebuilt/_tool_call_transformer.py +165 -0
- coco_graph/prebuilt/chat_agent_executor.py +1015 -0
- coco_graph/prebuilt/interrupt.py +105 -0
- coco_graph/prebuilt/py.typed +0 -0
- coco_graph/prebuilt/tool_node.py +2030 -0
- coco_graph/prebuilt/tool_validator.py +221 -0
- coco_graph/pregel/__init__.py +3 -0
- coco_graph/pregel/_algo.py +1460 -0
- coco_graph/pregel/_call.py +298 -0
- coco_graph/pregel/_checkpoint.py +331 -0
- coco_graph/pregel/_config.py +0 -0
- coco_graph/pregel/_draw.py +294 -0
- coco_graph/pregel/_executor.py +223 -0
- coco_graph/pregel/_io.py +174 -0
- coco_graph/pregel/_log.py +3 -0
- coco_graph/pregel/_loop.py +1988 -0
- coco_graph/pregel/_messages.py +461 -0
- coco_graph/pregel/_read.py +312 -0
- coco_graph/pregel/_retry.py +854 -0
- coco_graph/pregel/_runner.py +941 -0
- coco_graph/pregel/_tools.py +268 -0
- coco_graph/pregel/_utils.py +221 -0
- coco_graph/pregel/_validate.py +120 -0
- coco_graph/pregel/_write.py +192 -0
- coco_graph/pregel/debug.py +302 -0
- coco_graph/pregel/main.py +4364 -0
- coco_graph/pregel/protocol.py +288 -0
- coco_graph/pregel/types.py +38 -0
- coco_graph/py.typed +0 -0
- coco_graph/runtime.py +350 -0
- coco_graph/store/base/__init__.py +1322 -0
- coco_graph/store/base/batch.py +371 -0
- coco_graph/store/base/embed.py +436 -0
- coco_graph/store/base/py.typed +0 -0
- coco_graph/store/memory/__init__.py +592 -0
- coco_graph/store/memory/py.typed +0 -0
- coco_graph/store/postgres/__init__.py +4 -0
- coco_graph/store/postgres/aio.py +592 -0
- coco_graph/store/postgres/base.py +1480 -0
- coco_graph/store/postgres/py.typed +0 -0
- coco_graph/store/sqlite/__init__.py +4 -0
- coco_graph/store/sqlite/aio.py +632 -0
- coco_graph/store/sqlite/base.py +1514 -0
- coco_graph/stream/__init__.py +45 -0
- coco_graph/stream/_convert.py +32 -0
- coco_graph/stream/_mux.py +523 -0
- coco_graph/stream/_types.py +330 -0
- coco_graph/stream/run_stream.py +735 -0
- coco_graph/stream/stream_channel.py +341 -0
- coco_graph/stream/transformers.py +1039 -0
- coco_graph/types.py +1079 -0
- coco_graph/typing.py +48 -0
- coco_graph/utils/__init__.py +1 -0
- coco_graph/utils/config.py +4 -0
- coco_graph/utils/runnable.py +3 -0
- coco_graph/version.py +13 -0
- coco_graph/warnings.py +69 -0
- coco_openai/__init__.py +20 -0
- coco_openai/_compat.py +45 -0
- coco_openai/_version.py +3 -0
- coco_openai/chat_models/__init__.py +6 -0
- coco_openai/chat_models/_client_utils.py +684 -0
- coco_openai/chat_models/_compat.py +525 -0
- coco_openai/chat_models/azure.py +1227 -0
- coco_openai/chat_models/base.py +5654 -0
- coco_openai/chat_models/codex.py +557 -0
- coco_openai/chatgpt_oauth.py +1063 -0
- coco_openai/data/__init__.py +1 -0
- coco_openai/data/_profiles.py +1655 -0
- coco_openai/data/profile_augmentations.toml +127 -0
- coco_openai/embeddings/__init__.py +6 -0
- coco_openai/embeddings/azure.py +240 -0
- coco_openai/embeddings/base.py +820 -0
- coco_openai/llms/__init__.py +6 -0
- coco_openai/llms/azure.py +249 -0
- coco_openai/llms/base.py +919 -0
- coco_openai/middleware/__init__.py +11 -0
- coco_openai/middleware/openai_moderation.py +484 -0
- coco_openai/output_parsers/__init__.py +9 -0
- coco_openai/output_parsers/tools.py +9 -0
- coco_openai/py.typed +0 -0
- coco_openai/tools/__init__.py +5 -0
- coco_openai/tools/custom_tool.py +111 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""SSRF protection and security utilities.
|
|
2
|
+
|
|
3
|
+
This is an **internal** module (note the `_security` prefix). It is NOT part of
|
|
4
|
+
the public `trustcoco-core` API and may change or be removed at any time without
|
|
5
|
+
notice. External code should not import from or depend on anything in this
|
|
6
|
+
module. Any vulnerability reports should target the public APIs that use these
|
|
7
|
+
utilities, not this internal module directly.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from coco_core._security._exceptions import SSRFBlockedError
|
|
11
|
+
from coco_core._security._policy import (
|
|
12
|
+
SSRFPolicy,
|
|
13
|
+
validate_hostname,
|
|
14
|
+
validate_resolved_ip,
|
|
15
|
+
validate_url,
|
|
16
|
+
validate_url_sync,
|
|
17
|
+
)
|
|
18
|
+
from coco_core._security._transport import (
|
|
19
|
+
SSRFSafeSyncTransport,
|
|
20
|
+
SSRFSafeTransport,
|
|
21
|
+
ssrf_safe_async_client,
|
|
22
|
+
ssrf_safe_client,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"SSRFBlockedError",
|
|
27
|
+
"SSRFPolicy",
|
|
28
|
+
"SSRFSafeSyncTransport",
|
|
29
|
+
"SSRFSafeTransport",
|
|
30
|
+
"ssrf_safe_async_client",
|
|
31
|
+
"ssrf_safe_client",
|
|
32
|
+
"validate_hostname",
|
|
33
|
+
"validate_resolved_ip",
|
|
34
|
+
"validate_url",
|
|
35
|
+
"validate_url_sync",
|
|
36
|
+
]
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""SSRF protection policy with IP validation and DNS-aware URL checking."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import dataclasses
|
|
5
|
+
import ipaddress
|
|
6
|
+
import os
|
|
7
|
+
import socket
|
|
8
|
+
import urllib.parse
|
|
9
|
+
|
|
10
|
+
from coco_core._security._exceptions import SSRFBlockedError
|
|
11
|
+
|
|
12
|
+
# ---------------------------------------------------------------------------
|
|
13
|
+
# Blocklist constants
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
_BLOCKED_IPV4_NETWORKS: tuple[ipaddress.IPv4Network, ...] = tuple(
|
|
17
|
+
ipaddress.IPv4Network(n)
|
|
18
|
+
for n in (
|
|
19
|
+
"10.0.0.0/8", # RFC 1918 - private class A
|
|
20
|
+
"172.16.0.0/12", # RFC 1918 - private class B
|
|
21
|
+
"192.168.0.0/16", # RFC 1918 - private class C
|
|
22
|
+
"127.0.0.0/8", # RFC 1122 - loopback
|
|
23
|
+
"169.254.0.0/16", # RFC 3927 - link-local
|
|
24
|
+
"0.0.0.0/8", # RFC 1122 - "this network"
|
|
25
|
+
"100.64.0.0/10", # RFC 6598 - shared/CGN address space
|
|
26
|
+
"192.0.0.0/24", # RFC 6890 - IETF protocol assignments
|
|
27
|
+
"192.0.2.0/24", # RFC 5737 - TEST-NET-1 (documentation)
|
|
28
|
+
"198.18.0.0/15", # RFC 2544 - benchmarking
|
|
29
|
+
"198.51.100.0/24", # RFC 5737 - TEST-NET-2 (documentation)
|
|
30
|
+
"203.0.113.0/24", # RFC 5737 - TEST-NET-3 (documentation)
|
|
31
|
+
"224.0.0.0/4", # RFC 5771 - multicast
|
|
32
|
+
"240.0.0.0/4", # RFC 1112 - reserved for future use
|
|
33
|
+
"255.255.255.255/32", # RFC 919 - limited broadcast
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
_BLOCKED_IPV6_NETWORKS: tuple[ipaddress.IPv6Network, ...] = tuple(
|
|
38
|
+
ipaddress.IPv6Network(n)
|
|
39
|
+
for n in (
|
|
40
|
+
"::1/128", # RFC 4291 - loopback
|
|
41
|
+
"fc00::/7", # RFC 4193 - unique local addresses (ULA)
|
|
42
|
+
"fe80::/10", # RFC 4291 - link-local
|
|
43
|
+
"ff00::/8", # RFC 4291 - multicast
|
|
44
|
+
"::ffff:0:0/96", # RFC 4291 - IPv4-mapped IPv6 addresses
|
|
45
|
+
"::0.0.0.0/96", # RFC 4291 - IPv4-compatible IPv6 (deprecated)
|
|
46
|
+
"64:ff9b::/96", # RFC 6052 - NAT64 well-known prefix
|
|
47
|
+
"64:ff9b:1::/48", # RFC 8215 - NAT64 discovery prefix
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
_CLOUD_METADATA_IPS: frozenset[str] = frozenset(
|
|
52
|
+
{
|
|
53
|
+
"169.254.169.254", # AWS, GCP, Azure, DigitalOcean, Oracle Cloud
|
|
54
|
+
"169.254.170.2", # AWS ECS task metadata
|
|
55
|
+
"169.254.170.23", # AWS EKS Pod Identity Agent
|
|
56
|
+
"100.100.100.200", # Alibaba Cloud metadata
|
|
57
|
+
"fd00:ec2::254", # AWS EC2 IMDSv2 over IPv6 (Nitro instances)
|
|
58
|
+
"fd00:ec2::23", # AWS EKS Pod Identity Agent (IPv6)
|
|
59
|
+
"fe80::a9fe:a9fe", # OpenStack Nova metadata (IPv6 link-local)
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Network ranges that are always blocked when block_cloud_metadata=True,
|
|
64
|
+
# independent of block_private_ips. The entire link-local range is used by
|
|
65
|
+
# cloud metadata services across providers.
|
|
66
|
+
_CLOUD_METADATA_NETWORKS: tuple[ipaddress.IPv4Network | ipaddress.IPv6Network, ...] = (
|
|
67
|
+
ipaddress.IPv4Network("169.254.0.0/16"),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
_CLOUD_METADATA_HOSTNAMES: frozenset[str] = frozenset(
|
|
71
|
+
{
|
|
72
|
+
"metadata.google.internal",
|
|
73
|
+
"metadata.amazonaws.com",
|
|
74
|
+
"metadata",
|
|
75
|
+
"instance-data",
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
_LOCALHOST_NAMES: frozenset[str] = frozenset(
|
|
80
|
+
{
|
|
81
|
+
"localhost",
|
|
82
|
+
"localhost.localdomain",
|
|
83
|
+
"host.docker.internal",
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
_K8S_SUFFIX = ".svc.cluster.local"
|
|
88
|
+
|
|
89
|
+
_LOOPBACK_IPV4 = ipaddress.IPv4Network("127.0.0.0/8")
|
|
90
|
+
_LOOPBACK_IPV6 = ipaddress.IPv6Address("::1")
|
|
91
|
+
|
|
92
|
+
# NAT64 well-known prefixes
|
|
93
|
+
_NAT64_PREFIX = ipaddress.IPv6Network("64:ff9b::/96")
|
|
94
|
+
_NAT64_DISCOVERY_PREFIX = ipaddress.IPv6Network("64:ff9b:1::/48")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
# SSRFPolicy
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclasses.dataclass(frozen=True)
|
|
103
|
+
class SSRFPolicy:
|
|
104
|
+
"""Immutable policy controlling which URLs/IPs are considered safe."""
|
|
105
|
+
|
|
106
|
+
allowed_schemes: frozenset[str] = frozenset({"http", "https"})
|
|
107
|
+
block_private_ips: bool = True
|
|
108
|
+
block_localhost: bool = True
|
|
109
|
+
block_cloud_metadata: bool = True
|
|
110
|
+
block_k8s_internal: bool = True
|
|
111
|
+
allowed_hosts: frozenset[str] = frozenset()
|
|
112
|
+
additional_blocked_cidrs: tuple[
|
|
113
|
+
ipaddress.IPv4Network | ipaddress.IPv6Network, ...
|
|
114
|
+
] = ()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
DEFAULT_SSRF_POLICY = SSRFPolicy()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
# Helpers
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _extract_embedded_ipv4(
|
|
126
|
+
addr: ipaddress.IPv6Address,
|
|
127
|
+
) -> ipaddress.IPv4Address | None:
|
|
128
|
+
"""Extract an embedded IPv4 from IPv4-mapped or NAT64 IPv6 addresses."""
|
|
129
|
+
# Check ipv4_mapped first (covers ::ffff:x.x.x.x)
|
|
130
|
+
if addr.ipv4_mapped is not None:
|
|
131
|
+
return addr.ipv4_mapped
|
|
132
|
+
|
|
133
|
+
# Check NAT64 prefixes — embedded IPv4 is in the last 4 bytes
|
|
134
|
+
if addr in _NAT64_PREFIX or addr in _NAT64_DISCOVERY_PREFIX:
|
|
135
|
+
raw = addr.packed
|
|
136
|
+
return ipaddress.IPv4Address(raw[-4:])
|
|
137
|
+
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _ip_in_blocked_networks(
|
|
142
|
+
addr: ipaddress.IPv4Address | ipaddress.IPv6Address,
|
|
143
|
+
policy: SSRFPolicy,
|
|
144
|
+
) -> str | None:
|
|
145
|
+
"""Return a reason string if *addr* falls in a blocked range, else None."""
|
|
146
|
+
# NOTE: if profiling shows this is a hot path, consider memoising with
|
|
147
|
+
# @functools.lru_cache (key on (addr, id(policy))).
|
|
148
|
+
if isinstance(addr, ipaddress.IPv4Address):
|
|
149
|
+
if policy.block_private_ips:
|
|
150
|
+
for blocked_ipv4_net in _BLOCKED_IPV4_NETWORKS:
|
|
151
|
+
if addr in blocked_ipv4_net:
|
|
152
|
+
return "private IP range"
|
|
153
|
+
for blocked_cidr in policy.additional_blocked_cidrs:
|
|
154
|
+
if isinstance(blocked_cidr, ipaddress.IPv4Network) and addr in blocked_cidr:
|
|
155
|
+
return "blocked CIDR"
|
|
156
|
+
else:
|
|
157
|
+
if policy.block_private_ips:
|
|
158
|
+
for blocked_ipv6_net in _BLOCKED_IPV6_NETWORKS:
|
|
159
|
+
if addr in blocked_ipv6_net:
|
|
160
|
+
return "private IP range"
|
|
161
|
+
for blocked_cidr in policy.additional_blocked_cidrs:
|
|
162
|
+
if isinstance(blocked_cidr, ipaddress.IPv6Network) and addr in blocked_cidr:
|
|
163
|
+
return "blocked CIDR"
|
|
164
|
+
|
|
165
|
+
# Loopback check — independent of block_private_ips so that
|
|
166
|
+
# block_localhost=True still catches 127.x.x.x / ::1 even when
|
|
167
|
+
# private IPs are allowed.
|
|
168
|
+
if policy.block_localhost:
|
|
169
|
+
if isinstance(addr, ipaddress.IPv4Address) and (
|
|
170
|
+
addr in _LOOPBACK_IPV4 or addr in ipaddress.IPv4Network("0.0.0.0/8")
|
|
171
|
+
):
|
|
172
|
+
return "localhost address"
|
|
173
|
+
if isinstance(addr, ipaddress.IPv6Address) and addr == _LOOPBACK_IPV6:
|
|
174
|
+
return "localhost address"
|
|
175
|
+
|
|
176
|
+
# Cloud metadata check — IP set *and* network ranges (e.g. 169.254.0.0/16).
|
|
177
|
+
# Independent of block_private_ips so that allow_private=True still blocks
|
|
178
|
+
# cloud metadata endpoints.
|
|
179
|
+
if policy.block_cloud_metadata:
|
|
180
|
+
if str(addr) in _CLOUD_METADATA_IPS:
|
|
181
|
+
return "cloud metadata endpoint"
|
|
182
|
+
for net in _CLOUD_METADATA_NETWORKS:
|
|
183
|
+
if addr in net:
|
|
184
|
+
return "cloud metadata endpoint"
|
|
185
|
+
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# ---------------------------------------------------------------------------
|
|
190
|
+
# Public validation functions
|
|
191
|
+
# ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def validate_resolved_ip(ip_str: str, policy: SSRFPolicy) -> None:
|
|
195
|
+
"""Validate a resolved IP address against the SSRF policy.
|
|
196
|
+
|
|
197
|
+
Raises SSRFBlockedError if the IP is blocked.
|
|
198
|
+
"""
|
|
199
|
+
try:
|
|
200
|
+
addr = ipaddress.ip_address(ip_str)
|
|
201
|
+
except ValueError as exc:
|
|
202
|
+
msg = "invalid IP address"
|
|
203
|
+
raise SSRFBlockedError(msg) from exc
|
|
204
|
+
|
|
205
|
+
if isinstance(addr, ipaddress.IPv6Address):
|
|
206
|
+
inner = _extract_embedded_ipv4(addr)
|
|
207
|
+
if inner is not None:
|
|
208
|
+
addr = inner
|
|
209
|
+
|
|
210
|
+
reason = _ip_in_blocked_networks(addr, policy)
|
|
211
|
+
if reason is not None:
|
|
212
|
+
raise SSRFBlockedError(reason)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def validate_hostname(hostname: str, policy: SSRFPolicy) -> None:
|
|
216
|
+
"""Validate a hostname against the SSRF policy.
|
|
217
|
+
|
|
218
|
+
Raises SSRFBlockedError if the hostname is blocked.
|
|
219
|
+
"""
|
|
220
|
+
lower = hostname.lower()
|
|
221
|
+
|
|
222
|
+
if policy.block_localhost and lower in _LOCALHOST_NAMES:
|
|
223
|
+
msg = "localhost address"
|
|
224
|
+
raise SSRFBlockedError(msg)
|
|
225
|
+
|
|
226
|
+
if policy.block_cloud_metadata and lower in _CLOUD_METADATA_HOSTNAMES:
|
|
227
|
+
msg = "cloud metadata endpoint"
|
|
228
|
+
raise SSRFBlockedError(msg)
|
|
229
|
+
|
|
230
|
+
if policy.block_k8s_internal and lower.endswith(_K8S_SUFFIX):
|
|
231
|
+
msg = "Kubernetes internal DNS"
|
|
232
|
+
raise SSRFBlockedError(msg)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _effective_allowed_hosts(policy: SSRFPolicy) -> frozenset[str]:
|
|
236
|
+
"""Return allowed_hosts, augmented for local environments."""
|
|
237
|
+
extra: set[str] = set()
|
|
238
|
+
if os.environ.get("COCO_ENV", "").startswith("local"):
|
|
239
|
+
extra.update({"localhost", "testserver"})
|
|
240
|
+
if extra:
|
|
241
|
+
return policy.allowed_hosts | frozenset(extra)
|
|
242
|
+
return policy.allowed_hosts
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
async def validate_url(url: str, policy: SSRFPolicy = DEFAULT_SSRF_POLICY) -> None:
|
|
246
|
+
"""Validate a URL against the SSRF policy, including DNS resolution.
|
|
247
|
+
|
|
248
|
+
This is the primary entry-point for async code paths. It delegates
|
|
249
|
+
scheme/hostname/allowed-hosts checks to `validate_url_sync`, then
|
|
250
|
+
resolves DNS and validates every resolved IP.
|
|
251
|
+
|
|
252
|
+
Raises:
|
|
253
|
+
SSRFBlockedError: If the URL violates the policy.
|
|
254
|
+
"""
|
|
255
|
+
parsed = urllib.parse.urlparse(url)
|
|
256
|
+
hostname = parsed.hostname or ""
|
|
257
|
+
|
|
258
|
+
validate_url_sync(url, policy)
|
|
259
|
+
|
|
260
|
+
allowed = {h.lower() for h in _effective_allowed_hosts(policy)}
|
|
261
|
+
if hostname.lower() in allowed:
|
|
262
|
+
return
|
|
263
|
+
|
|
264
|
+
scheme = (parsed.scheme or "").lower()
|
|
265
|
+
port = parsed.port or (443 if scheme == "https" else 80)
|
|
266
|
+
try:
|
|
267
|
+
addrinfo = await asyncio.to_thread(
|
|
268
|
+
socket.getaddrinfo, hostname, port, type=socket.SOCK_STREAM
|
|
269
|
+
)
|
|
270
|
+
except socket.gaierror as exc:
|
|
271
|
+
msg = "DNS resolution failed"
|
|
272
|
+
raise SSRFBlockedError(msg) from exc
|
|
273
|
+
|
|
274
|
+
for _family, _type, _proto, _canonname, sockaddr in addrinfo:
|
|
275
|
+
validate_resolved_ip(str(sockaddr[0]), policy)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def validate_url_sync(url: str, policy: SSRFPolicy = DEFAULT_SSRF_POLICY) -> None:
|
|
279
|
+
"""Synchronous URL validation (no DNS resolution).
|
|
280
|
+
|
|
281
|
+
Suitable for Pydantic validators and other sync contexts. Checks scheme
|
|
282
|
+
and hostname patterns only - use `validate_url` for full DNS-aware checking.
|
|
283
|
+
|
|
284
|
+
Raises:
|
|
285
|
+
SSRFBlockedError: If the URL violates the policy.
|
|
286
|
+
"""
|
|
287
|
+
parsed = urllib.parse.urlparse(url)
|
|
288
|
+
|
|
289
|
+
scheme = (parsed.scheme or "").lower()
|
|
290
|
+
if scheme not in policy.allowed_schemes:
|
|
291
|
+
msg = f"scheme '{scheme}' not allowed"
|
|
292
|
+
raise SSRFBlockedError(msg)
|
|
293
|
+
|
|
294
|
+
hostname = parsed.hostname
|
|
295
|
+
if not hostname:
|
|
296
|
+
msg = "missing hostname"
|
|
297
|
+
raise SSRFBlockedError(msg)
|
|
298
|
+
|
|
299
|
+
allowed = _effective_allowed_hosts(policy)
|
|
300
|
+
if hostname.lower() in {h.lower() for h in allowed}:
|
|
301
|
+
return
|
|
302
|
+
|
|
303
|
+
try:
|
|
304
|
+
ipaddress.ip_address(hostname)
|
|
305
|
+
validate_resolved_ip(hostname, policy)
|
|
306
|
+
except SSRFBlockedError:
|
|
307
|
+
raise
|
|
308
|
+
except ValueError:
|
|
309
|
+
pass
|
|
310
|
+
else:
|
|
311
|
+
return
|
|
312
|
+
|
|
313
|
+
validate_hostname(hostname, policy)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""SSRF Protection - thin wrapper raising ValueError for internal callers.
|
|
2
|
+
|
|
3
|
+
Delegates all validation to `coco_core._security._policy`.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import socket
|
|
8
|
+
from typing import Annotated, Any
|
|
9
|
+
from urllib.parse import urlparse
|
|
10
|
+
|
|
11
|
+
from pydantic import (
|
|
12
|
+
AnyHttpUrl,
|
|
13
|
+
BeforeValidator,
|
|
14
|
+
HttpUrl,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from coco_core._security._exceptions import SSRFBlockedError
|
|
18
|
+
from coco_core._security._policy import (
|
|
19
|
+
SSRFPolicy,
|
|
20
|
+
)
|
|
21
|
+
from coco_core._security._policy import (
|
|
22
|
+
validate_resolved_ip as _validate_resolved_ip,
|
|
23
|
+
)
|
|
24
|
+
from coco_core._security._policy import (
|
|
25
|
+
validate_url_sync as _validate_url_sync,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _policy_for(*, allow_private: bool, allow_http: bool) -> SSRFPolicy:
|
|
30
|
+
"""Build an `SSRFPolicy` from the legacy flag interface."""
|
|
31
|
+
schemes = frozenset({"http", "https"}) if allow_http else frozenset({"https"})
|
|
32
|
+
return SSRFPolicy(
|
|
33
|
+
allowed_schemes=schemes,
|
|
34
|
+
block_private_ips=not allow_private,
|
|
35
|
+
block_localhost=not allow_private,
|
|
36
|
+
block_cloud_metadata=True,
|
|
37
|
+
block_k8s_internal=True,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def validate_safe_url(
|
|
42
|
+
url: str | AnyHttpUrl,
|
|
43
|
+
*,
|
|
44
|
+
allow_private: bool = False,
|
|
45
|
+
allow_http: bool = True,
|
|
46
|
+
) -> str:
|
|
47
|
+
"""Validate a URL for SSRF protection.
|
|
48
|
+
|
|
49
|
+
This function validates URLs to prevent Server-Side Request Forgery (SSRF) attacks
|
|
50
|
+
by blocking requests to private networks and cloud metadata endpoints.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
url: The URL to validate (string or Pydantic HttpUrl).
|
|
54
|
+
allow_private: If `True`, allows private IPs and localhost (for development).
|
|
55
|
+
Cloud metadata endpoints are ALWAYS blocked.
|
|
56
|
+
allow_http: If `True`, allows both HTTP and HTTPS. If `False`, only HTTPS.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
The validated URL as a string.
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
ValueError: If URL is invalid or potentially dangerous.
|
|
63
|
+
"""
|
|
64
|
+
url_str = str(url)
|
|
65
|
+
parsed = urlparse(url_str)
|
|
66
|
+
hostname = parsed.hostname or ""
|
|
67
|
+
|
|
68
|
+
# Test-environment bypass (preserved from original implementation)
|
|
69
|
+
if (
|
|
70
|
+
os.environ.get("COCO_ENV") == "local_test"
|
|
71
|
+
and hostname.startswith("test")
|
|
72
|
+
and "server" in hostname
|
|
73
|
+
):
|
|
74
|
+
return url_str
|
|
75
|
+
|
|
76
|
+
policy = _policy_for(allow_private=allow_private, allow_http=allow_http)
|
|
77
|
+
|
|
78
|
+
# Synchronous scheme + hostname checks
|
|
79
|
+
try:
|
|
80
|
+
_validate_url_sync(url_str, policy)
|
|
81
|
+
except SSRFBlockedError as exc:
|
|
82
|
+
raise ValueError(str(exc)) from exc
|
|
83
|
+
|
|
84
|
+
# DNS resolution and IP validation
|
|
85
|
+
try:
|
|
86
|
+
addr_info = socket.getaddrinfo(
|
|
87
|
+
hostname,
|
|
88
|
+
parsed.port or (443 if parsed.scheme == "https" else 80),
|
|
89
|
+
socket.AF_UNSPEC,
|
|
90
|
+
socket.SOCK_STREAM,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
for result in addr_info:
|
|
94
|
+
ip_str: str = result[4][0] # type: ignore[assignment]
|
|
95
|
+
try:
|
|
96
|
+
_validate_resolved_ip(ip_str, policy)
|
|
97
|
+
except SSRFBlockedError as exc:
|
|
98
|
+
raise ValueError(str(exc)) from exc
|
|
99
|
+
|
|
100
|
+
except socket.gaierror as e:
|
|
101
|
+
msg = f"Failed to resolve hostname '{hostname}': {e}"
|
|
102
|
+
raise ValueError(msg) from e
|
|
103
|
+
except OSError as e:
|
|
104
|
+
msg = f"Network error while validating URL: {e}"
|
|
105
|
+
raise ValueError(msg) from e
|
|
106
|
+
|
|
107
|
+
return url_str
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def is_safe_url(
|
|
111
|
+
url: str | AnyHttpUrl,
|
|
112
|
+
*,
|
|
113
|
+
allow_private: bool = False,
|
|
114
|
+
allow_http: bool = True,
|
|
115
|
+
) -> bool:
|
|
116
|
+
"""Non-throwing version of `validate_safe_url`."""
|
|
117
|
+
try:
|
|
118
|
+
validate_safe_url(url, allow_private=allow_private, allow_http=allow_http)
|
|
119
|
+
except ValueError:
|
|
120
|
+
return False
|
|
121
|
+
else:
|
|
122
|
+
return True
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _validate_url_ssrf_strict(v: Any) -> Any:
|
|
126
|
+
"""Validate URL for SSRF protection (strict mode)."""
|
|
127
|
+
if isinstance(v, str):
|
|
128
|
+
validate_safe_url(v, allow_private=False, allow_http=True)
|
|
129
|
+
return v
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _validate_url_ssrf_https_only(v: Any) -> Any:
|
|
133
|
+
if isinstance(v, str):
|
|
134
|
+
validate_safe_url(v, allow_private=False, allow_http=False)
|
|
135
|
+
return v
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _validate_url_ssrf_relaxed(v: Any) -> Any:
|
|
139
|
+
"""Validate URL for SSRF protection (relaxed mode - allows private IPs)."""
|
|
140
|
+
if isinstance(v, str):
|
|
141
|
+
validate_safe_url(v, allow_private=True, allow_http=True)
|
|
142
|
+
return v
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# Annotated types with SSRF protection
|
|
146
|
+
SSRFProtectedUrl = Annotated[HttpUrl, BeforeValidator(_validate_url_ssrf_strict)]
|
|
147
|
+
SSRFProtectedUrlRelaxed = Annotated[
|
|
148
|
+
HttpUrl, BeforeValidator(_validate_url_ssrf_relaxed)
|
|
149
|
+
]
|
|
150
|
+
SSRFProtectedHttpsUrl = Annotated[
|
|
151
|
+
HttpUrl, BeforeValidator(_validate_url_ssrf_https_only)
|
|
152
|
+
]
|
|
153
|
+
SSRFProtectedHttpsUrlStr = Annotated[
|
|
154
|
+
str, BeforeValidator(_validate_url_ssrf_https_only)
|
|
155
|
+
]
|