lfx-nightly 0.2.0.dev0__py3-none-any.whl → 0.2.0.dev41__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.
- lfx/_assets/component_index.json +1 -1
- lfx/base/agents/agent.py +21 -4
- lfx/base/agents/altk_base_agent.py +393 -0
- lfx/base/agents/altk_tool_wrappers.py +565 -0
- lfx/base/agents/events.py +2 -1
- lfx/base/composio/composio_base.py +159 -224
- lfx/base/data/base_file.py +97 -20
- lfx/base/data/docling_utils.py +61 -10
- lfx/base/data/storage_utils.py +301 -0
- lfx/base/data/utils.py +178 -14
- lfx/base/mcp/util.py +2 -2
- lfx/base/models/anthropic_constants.py +21 -12
- lfx/base/models/groq_constants.py +74 -58
- lfx/base/models/groq_model_discovery.py +265 -0
- lfx/base/models/model.py +1 -1
- lfx/base/models/model_utils.py +100 -0
- lfx/base/models/openai_constants.py +7 -0
- lfx/base/models/watsonx_constants.py +32 -8
- lfx/base/tools/run_flow.py +601 -129
- lfx/cli/commands.py +9 -4
- lfx/cli/common.py +2 -2
- lfx/cli/run.py +1 -1
- lfx/cli/script_loader.py +53 -11
- lfx/components/Notion/create_page.py +1 -1
- lfx/components/Notion/list_database_properties.py +1 -1
- lfx/components/Notion/list_pages.py +1 -1
- lfx/components/Notion/list_users.py +1 -1
- lfx/components/Notion/page_content_viewer.py +1 -1
- lfx/components/Notion/search.py +1 -1
- lfx/components/Notion/update_page_property.py +1 -1
- lfx/components/__init__.py +19 -5
- lfx/components/{agents → altk}/__init__.py +5 -9
- lfx/components/altk/altk_agent.py +193 -0
- lfx/components/apify/apify_actor.py +1 -1
- lfx/components/composio/__init__.py +70 -18
- lfx/components/composio/apollo_composio.py +11 -0
- lfx/components/composio/bitbucket_composio.py +11 -0
- lfx/components/composio/canva_composio.py +11 -0
- lfx/components/composio/coda_composio.py +11 -0
- lfx/components/composio/composio_api.py +10 -0
- lfx/components/composio/discord_composio.py +1 -1
- lfx/components/composio/elevenlabs_composio.py +11 -0
- lfx/components/composio/exa_composio.py +11 -0
- lfx/components/composio/firecrawl_composio.py +11 -0
- lfx/components/composio/fireflies_composio.py +11 -0
- lfx/components/composio/gmail_composio.py +1 -1
- lfx/components/composio/googlebigquery_composio.py +11 -0
- lfx/components/composio/googlecalendar_composio.py +1 -1
- lfx/components/composio/googledocs_composio.py +1 -1
- lfx/components/composio/googlemeet_composio.py +1 -1
- lfx/components/composio/googlesheets_composio.py +1 -1
- lfx/components/composio/googletasks_composio.py +1 -1
- lfx/components/composio/heygen_composio.py +11 -0
- lfx/components/composio/mem0_composio.py +11 -0
- lfx/components/composio/peopledatalabs_composio.py +11 -0
- lfx/components/composio/perplexityai_composio.py +11 -0
- lfx/components/composio/serpapi_composio.py +11 -0
- lfx/components/composio/slack_composio.py +3 -574
- lfx/components/composio/slackbot_composio.py +1 -1
- lfx/components/composio/snowflake_composio.py +11 -0
- lfx/components/composio/tavily_composio.py +11 -0
- lfx/components/composio/youtube_composio.py +2 -2
- lfx/components/cuga/__init__.py +34 -0
- lfx/components/cuga/cuga_agent.py +730 -0
- lfx/components/data/__init__.py +78 -28
- lfx/components/data_source/__init__.py +58 -0
- lfx/components/{data → data_source}/api_request.py +26 -3
- lfx/components/{data → data_source}/csv_to_data.py +15 -10
- lfx/components/{data → data_source}/json_to_data.py +15 -8
- lfx/components/{data → data_source}/news_search.py +1 -1
- lfx/components/{data → data_source}/rss.py +1 -1
- lfx/components/{data → data_source}/sql_executor.py +1 -1
- lfx/components/{data → data_source}/url.py +1 -1
- lfx/components/{data → data_source}/web_search.py +1 -1
- lfx/components/datastax/astradb_cql.py +1 -1
- lfx/components/datastax/astradb_graph.py +1 -1
- lfx/components/datastax/astradb_tool.py +1 -1
- lfx/components/datastax/astradb_vectorstore.py +1 -1
- lfx/components/datastax/hcd.py +1 -1
- lfx/components/deactivated/json_document_builder.py +1 -1
- lfx/components/docling/__init__.py +0 -3
- lfx/components/docling/chunk_docling_document.py +3 -1
- lfx/components/docling/export_docling_document.py +3 -1
- lfx/components/elastic/elasticsearch.py +1 -1
- lfx/components/files_and_knowledge/__init__.py +47 -0
- lfx/components/{data → files_and_knowledge}/directory.py +1 -1
- lfx/components/{data → files_and_knowledge}/file.py +304 -24
- lfx/components/{knowledge_bases → files_and_knowledge}/retrieval.py +2 -2
- lfx/components/{data → files_and_knowledge}/save_file.py +218 -31
- lfx/components/flow_controls/__init__.py +58 -0
- lfx/components/{logic → flow_controls}/conditional_router.py +1 -1
- lfx/components/{logic → flow_controls}/loop.py +43 -9
- lfx/components/flow_controls/run_flow.py +108 -0
- lfx/components/glean/glean_search_api.py +1 -1
- lfx/components/groq/groq.py +35 -28
- lfx/components/helpers/__init__.py +102 -0
- lfx/components/ibm/watsonx.py +7 -1
- lfx/components/input_output/__init__.py +3 -1
- lfx/components/input_output/chat.py +4 -3
- lfx/components/input_output/chat_output.py +10 -4
- lfx/components/input_output/text.py +1 -1
- lfx/components/input_output/text_output.py +1 -1
- lfx/components/{data → input_output}/webhook.py +1 -1
- lfx/components/knowledge_bases/__init__.py +59 -4
- lfx/components/langchain_utilities/character.py +1 -1
- lfx/components/langchain_utilities/csv_agent.py +84 -16
- lfx/components/langchain_utilities/json_agent.py +67 -12
- lfx/components/langchain_utilities/language_recursive.py +1 -1
- lfx/components/llm_operations/__init__.py +46 -0
- lfx/components/{processing → llm_operations}/batch_run.py +17 -8
- lfx/components/{processing → llm_operations}/lambda_filter.py +1 -1
- lfx/components/{logic → llm_operations}/llm_conditional_router.py +1 -1
- lfx/components/{processing/llm_router.py → llm_operations/llm_selector.py} +3 -3
- lfx/components/{processing → llm_operations}/structured_output.py +1 -1
- lfx/components/logic/__init__.py +126 -0
- lfx/components/mem0/mem0_chat_memory.py +11 -0
- lfx/components/models/__init__.py +64 -9
- lfx/components/models_and_agents/__init__.py +49 -0
- lfx/components/{agents → models_and_agents}/agent.py +6 -4
- lfx/components/models_and_agents/embedding_model.py +353 -0
- lfx/components/models_and_agents/language_model.py +398 -0
- lfx/components/{agents → models_and_agents}/mcp_component.py +53 -44
- lfx/components/{helpers → models_and_agents}/memory.py +1 -1
- lfx/components/nvidia/system_assist.py +1 -1
- lfx/components/olivya/olivya.py +1 -1
- lfx/components/ollama/ollama.py +24 -5
- lfx/components/processing/__init__.py +9 -60
- lfx/components/processing/converter.py +1 -1
- lfx/components/processing/dataframe_operations.py +1 -1
- lfx/components/processing/parse_json_data.py +2 -2
- lfx/components/processing/parser.py +1 -1
- lfx/components/processing/split_text.py +1 -1
- lfx/components/qdrant/qdrant.py +1 -1
- lfx/components/redis/redis.py +1 -1
- lfx/components/twelvelabs/split_video.py +10 -0
- lfx/components/twelvelabs/video_file.py +12 -0
- lfx/components/utilities/__init__.py +43 -0
- lfx/components/{helpers → utilities}/calculator_core.py +1 -1
- lfx/components/{helpers → utilities}/current_date.py +1 -1
- lfx/components/{processing → utilities}/python_repl_core.py +1 -1
- lfx/components/vectorstores/local_db.py +9 -0
- lfx/components/youtube/youtube_transcripts.py +118 -30
- lfx/custom/custom_component/component.py +57 -1
- lfx/custom/custom_component/custom_component.py +68 -6
- lfx/custom/directory_reader/directory_reader.py +5 -2
- lfx/graph/edge/base.py +43 -20
- lfx/graph/state/model.py +15 -2
- lfx/graph/utils.py +6 -0
- lfx/graph/vertex/param_handler.py +10 -7
- lfx/helpers/__init__.py +12 -0
- lfx/helpers/flow.py +117 -0
- lfx/inputs/input_mixin.py +24 -1
- lfx/inputs/inputs.py +13 -1
- lfx/interface/components.py +161 -83
- lfx/log/logger.py +5 -3
- lfx/schema/image.py +2 -12
- lfx/services/database/__init__.py +5 -0
- lfx/services/database/service.py +25 -0
- lfx/services/deps.py +87 -22
- lfx/services/interfaces.py +5 -0
- lfx/services/manager.py +24 -10
- lfx/services/mcp_composer/service.py +1029 -162
- lfx/services/session.py +5 -0
- lfx/services/settings/auth.py +18 -11
- lfx/services/settings/base.py +56 -30
- lfx/services/settings/constants.py +8 -0
- lfx/services/storage/local.py +108 -46
- lfx/services/storage/service.py +171 -29
- lfx/template/field/base.py +3 -0
- lfx/utils/image.py +29 -11
- lfx/utils/ssrf_protection.py +384 -0
- lfx/utils/validate_cloud.py +26 -0
- {lfx_nightly-0.2.0.dev0.dist-info → lfx_nightly-0.2.0.dev41.dist-info}/METADATA +38 -22
- {lfx_nightly-0.2.0.dev0.dist-info → lfx_nightly-0.2.0.dev41.dist-info}/RECORD +189 -160
- {lfx_nightly-0.2.0.dev0.dist-info → lfx_nightly-0.2.0.dev41.dist-info}/WHEEL +1 -1
- lfx/components/agents/altk_agent.py +0 -366
- lfx/components/agents/cuga_agent.py +0 -1013
- lfx/components/docling/docling_remote_vlm.py +0 -284
- lfx/components/logic/run_flow.py +0 -71
- lfx/components/models/embedding_model.py +0 -195
- lfx/components/models/language_model.py +0 -144
- lfx/components/processing/dataframe_to_toolset.py +0 -259
- /lfx/components/{data → data_source}/mock_data.py +0 -0
- /lfx/components/{knowledge_bases → files_and_knowledge}/ingestion.py +0 -0
- /lfx/components/{logic → flow_controls}/data_conditional_router.py +0 -0
- /lfx/components/{logic → flow_controls}/flow_tool.py +0 -0
- /lfx/components/{logic → flow_controls}/listen.py +0 -0
- /lfx/components/{logic → flow_controls}/notify.py +0 -0
- /lfx/components/{logic → flow_controls}/pass_message.py +0 -0
- /lfx/components/{logic → flow_controls}/sub_flow.py +0 -0
- /lfx/components/{processing → models_and_agents}/prompt.py +0 -0
- /lfx/components/{helpers → processing}/create_list.py +0 -0
- /lfx/components/{helpers → processing}/output_parser.py +0 -0
- /lfx/components/{helpers → processing}/store_message.py +0 -0
- /lfx/components/{helpers → utilities}/id_generator.py +0 -0
- {lfx_nightly-0.2.0.dev0.dist-info → lfx_nightly-0.2.0.dev41.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
"""SSRF (Server-Side Request Forgery) protection utilities.
|
|
2
|
+
|
|
3
|
+
This module provides validation to prevent SSRF attacks by blocking requests to:
|
|
4
|
+
- Private IP ranges (RFC 1918)
|
|
5
|
+
- Loopback addresses
|
|
6
|
+
- Cloud metadata endpoints (169.254.169.254)
|
|
7
|
+
- Other internal/special-use addresses
|
|
8
|
+
|
|
9
|
+
IMPORTANT: HTTP Redirects
|
|
10
|
+
According to OWASP SSRF Prevention Cheat Sheet, HTTP redirects should be DISABLED
|
|
11
|
+
to prevent bypass attacks where a public URL redirects to internal resources.
|
|
12
|
+
The API Request component has (as of v1.7.0) follow_redirects=False by default.
|
|
13
|
+
See: https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html
|
|
14
|
+
|
|
15
|
+
Configuration:
|
|
16
|
+
LANGFLOW_SSRF_PROTECTION_ENABLED: Enable/disable SSRF protection (default: false)
|
|
17
|
+
TODO: Change default to true in next major version (2.0)
|
|
18
|
+
LANGFLOW_SSRF_ALLOWED_HOSTS: Comma-separated list of allowed hosts/CIDR ranges
|
|
19
|
+
Examples: "192.168.1.0/24,internal-api.company.local,10.0.0.5"
|
|
20
|
+
|
|
21
|
+
TODO: In next major version (2.0):
|
|
22
|
+
- Change LANGFLOW_SSRF_PROTECTION_ENABLED default to "true"
|
|
23
|
+
- Remove warning-only mode and enforce blocking
|
|
24
|
+
- Update documentation to reflect breaking change
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import functools
|
|
28
|
+
import ipaddress
|
|
29
|
+
import socket
|
|
30
|
+
from urllib.parse import urlparse
|
|
31
|
+
|
|
32
|
+
from lfx.logging import logger
|
|
33
|
+
from lfx.services.deps import get_settings_service
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SSRFProtectionError(ValueError):
|
|
37
|
+
"""Raised when a URL is blocked due to SSRF protection."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@functools.cache
|
|
41
|
+
def get_blocked_ip_ranges() -> list[ipaddress.IPv4Network | ipaddress.IPv6Network]:
|
|
42
|
+
"""Get the list of blocked IP ranges, initializing lazily on first access.
|
|
43
|
+
|
|
44
|
+
This lazy loading avoids the startup cost of creating all ip_network objects
|
|
45
|
+
at module import time.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
list: List of blocked IPv4 and IPv6 network ranges.
|
|
49
|
+
"""
|
|
50
|
+
return [
|
|
51
|
+
# IPv4 ranges
|
|
52
|
+
ipaddress.ip_network("0.0.0.0/8"), # Current network (only valid as source)
|
|
53
|
+
ipaddress.ip_network("10.0.0.0/8"), # Private network (RFC 1918)
|
|
54
|
+
ipaddress.ip_network("100.64.0.0/10"), # Carrier-grade NAT (RFC 6598)
|
|
55
|
+
ipaddress.ip_network("127.0.0.0/8"), # Loopback
|
|
56
|
+
ipaddress.ip_network("169.254.0.0/16"), # Link-local / AWS metadata
|
|
57
|
+
ipaddress.ip_network("172.16.0.0/12"), # Private network (RFC 1918)
|
|
58
|
+
ipaddress.ip_network("192.0.0.0/24"), # IETF Protocol Assignments
|
|
59
|
+
ipaddress.ip_network("192.0.2.0/24"), # Documentation (TEST-NET-1)
|
|
60
|
+
ipaddress.ip_network("192.168.0.0/16"), # Private network (RFC 1918)
|
|
61
|
+
ipaddress.ip_network("198.18.0.0/15"), # Benchmarking
|
|
62
|
+
ipaddress.ip_network("198.51.100.0/24"), # Documentation (TEST-NET-2)
|
|
63
|
+
ipaddress.ip_network("203.0.113.0/24"), # Documentation (TEST-NET-3)
|
|
64
|
+
ipaddress.ip_network("224.0.0.0/4"), # Multicast
|
|
65
|
+
ipaddress.ip_network("240.0.0.0/4"), # Reserved
|
|
66
|
+
ipaddress.ip_network("255.255.255.255/32"), # Broadcast
|
|
67
|
+
# IPv6 ranges
|
|
68
|
+
ipaddress.ip_network("::1/128"), # Loopback
|
|
69
|
+
ipaddress.ip_network("::/128"), # Unspecified address
|
|
70
|
+
ipaddress.ip_network("::ffff:0:0/96"), # IPv4-mapped IPv6 addresses
|
|
71
|
+
ipaddress.ip_network("100::/64"), # Discard prefix
|
|
72
|
+
ipaddress.ip_network("2001::/23"), # IETF Protocol Assignments
|
|
73
|
+
ipaddress.ip_network("2001:db8::/32"), # Documentation
|
|
74
|
+
ipaddress.ip_network("fc00::/7"), # Unique local addresses (ULA)
|
|
75
|
+
ipaddress.ip_network("fe80::/10"), # Link-local
|
|
76
|
+
ipaddress.ip_network("ff00::/8"), # Multicast
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def is_ssrf_protection_enabled() -> bool:
|
|
81
|
+
"""Check if SSRF protection is enabled in settings.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
bool: True if SSRF protection is enabled, False otherwise.
|
|
85
|
+
"""
|
|
86
|
+
return get_settings_service().settings.ssrf_protection_enabled
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_allowed_hosts() -> list[str]:
|
|
90
|
+
"""Get list of allowed hosts and/or CIDR ranges for SSRF protection.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
list[str]: Stripped hostnames or CIDR blocks from settings, or empty list if unset.
|
|
94
|
+
"""
|
|
95
|
+
allowed_hosts = get_settings_service().settings.ssrf_allowed_hosts
|
|
96
|
+
if not allowed_hosts:
|
|
97
|
+
return []
|
|
98
|
+
# ssrf_allowed_hosts is already a list[str], just clean and filter entries
|
|
99
|
+
return [host.strip() for host in allowed_hosts if host and host.strip()]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def is_host_allowed(hostname: str, ip: str | None = None) -> bool:
|
|
103
|
+
"""Check if a hostname or IP is in the allowed hosts list.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
hostname: Hostname to check
|
|
107
|
+
ip: Optional IP address to check
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
bool: True if hostname or IP is in the allowed list, False otherwise.
|
|
111
|
+
"""
|
|
112
|
+
allowed_hosts = get_allowed_hosts()
|
|
113
|
+
if not allowed_hosts:
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
# Check hostname match
|
|
117
|
+
if hostname in allowed_hosts:
|
|
118
|
+
return True
|
|
119
|
+
|
|
120
|
+
# Check if hostname matches any wildcard patterns
|
|
121
|
+
for allowed in allowed_hosts:
|
|
122
|
+
if allowed.startswith("*."):
|
|
123
|
+
# Wildcard domain matching
|
|
124
|
+
domain_suffix = allowed[1:] # Remove the *
|
|
125
|
+
if hostname.endswith(domain_suffix) or hostname == domain_suffix[1:]:
|
|
126
|
+
return True
|
|
127
|
+
|
|
128
|
+
# Check IP-based matching if IP is provided
|
|
129
|
+
if ip:
|
|
130
|
+
try:
|
|
131
|
+
ip_obj = ipaddress.ip_address(ip)
|
|
132
|
+
|
|
133
|
+
# Check exact IP match
|
|
134
|
+
if ip in allowed_hosts:
|
|
135
|
+
return True
|
|
136
|
+
|
|
137
|
+
# Check CIDR range match
|
|
138
|
+
for allowed in allowed_hosts:
|
|
139
|
+
try:
|
|
140
|
+
# Try to parse as CIDR network
|
|
141
|
+
if "/" in allowed:
|
|
142
|
+
network = ipaddress.ip_network(allowed, strict=False)
|
|
143
|
+
if ip_obj in network:
|
|
144
|
+
return True
|
|
145
|
+
except (ValueError, ipaddress.AddressValueError):
|
|
146
|
+
# Not a valid CIDR, skip
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
except (ValueError, ipaddress.AddressValueError):
|
|
150
|
+
# Invalid IP, skip IP-based checks
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def is_ip_blocked(ip: str | ipaddress.IPv4Address | ipaddress.IPv6Address) -> bool:
|
|
157
|
+
"""Check if an IP address is in a blocked range.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
ip: IP address to check (string or ipaddress object)
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
bool: True if IP is in a blocked range, False otherwise.
|
|
164
|
+
"""
|
|
165
|
+
try:
|
|
166
|
+
ip_obj = ipaddress.ip_address(ip) if isinstance(ip, str) else ip
|
|
167
|
+
|
|
168
|
+
# Check against all blocked ranges
|
|
169
|
+
return any(ip_obj in blocked_range for blocked_range in get_blocked_ip_ranges())
|
|
170
|
+
except (ValueError, ipaddress.AddressValueError):
|
|
171
|
+
# If we can't parse the IP, treat it as blocked for safety
|
|
172
|
+
return True
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def resolve_hostname(hostname: str) -> list[str]:
|
|
176
|
+
"""Resolve a hostname to its IP addresses.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
hostname: Hostname to resolve
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
list[str]: List of resolved IP addresses
|
|
183
|
+
|
|
184
|
+
Raises:
|
|
185
|
+
SSRFProtectionError: If hostname cannot be resolved
|
|
186
|
+
"""
|
|
187
|
+
try:
|
|
188
|
+
# Get address info for both IPv4 and IPv6
|
|
189
|
+
addr_info = socket.getaddrinfo(hostname, None)
|
|
190
|
+
|
|
191
|
+
# Extract unique IP addresses
|
|
192
|
+
ips = []
|
|
193
|
+
for info in addr_info:
|
|
194
|
+
ip = info[4][0]
|
|
195
|
+
# Remove IPv6 zone ID if present (e.g., "fe80::1%eth0" -> "fe80::1")
|
|
196
|
+
if "%" in ip:
|
|
197
|
+
ip = ip.split("%")[0]
|
|
198
|
+
if ip not in ips:
|
|
199
|
+
ips.append(ip)
|
|
200
|
+
|
|
201
|
+
if not ips:
|
|
202
|
+
msg = f"Unable to resolve hostname: {hostname}"
|
|
203
|
+
raise SSRFProtectionError(msg)
|
|
204
|
+
except socket.gaierror as e:
|
|
205
|
+
msg = f"DNS resolution failed for {hostname}: {e}"
|
|
206
|
+
raise SSRFProtectionError(msg) from e
|
|
207
|
+
except Exception as e:
|
|
208
|
+
msg = f"Error resolving hostname {hostname}: {e}"
|
|
209
|
+
raise SSRFProtectionError(msg) from e
|
|
210
|
+
|
|
211
|
+
return ips
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _validate_url_scheme(scheme: str) -> None:
|
|
215
|
+
"""Validate that URL scheme is http or https.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
scheme: URL scheme to validate
|
|
219
|
+
|
|
220
|
+
Raises:
|
|
221
|
+
SSRFProtectionError: If scheme is invalid
|
|
222
|
+
"""
|
|
223
|
+
if scheme not in ("http", "https"):
|
|
224
|
+
msg = f"Invalid URL scheme '{scheme}'. Only http and https are allowed."
|
|
225
|
+
raise SSRFProtectionError(msg)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _validate_hostname_exists(hostname: str | None) -> str:
|
|
229
|
+
"""Validate that hostname exists in the URL.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
hostname: Hostname to validate (may be None)
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
str: The validated hostname
|
|
236
|
+
|
|
237
|
+
Raises:
|
|
238
|
+
SSRFProtectionError: If hostname is missing
|
|
239
|
+
"""
|
|
240
|
+
if not hostname:
|
|
241
|
+
msg = "URL must contain a valid hostname"
|
|
242
|
+
raise SSRFProtectionError(msg)
|
|
243
|
+
return hostname
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _validate_direct_ip_address(hostname: str) -> bool:
|
|
247
|
+
"""Validate a direct IP address in the URL.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
hostname: Hostname that may be an IP address
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
bool: True if hostname is a direct IP and validation passed,
|
|
254
|
+
False if hostname is not an IP (caller should continue with DNS resolution)
|
|
255
|
+
|
|
256
|
+
Raises:
|
|
257
|
+
SSRFProtectionError: If IP is blocked
|
|
258
|
+
"""
|
|
259
|
+
try:
|
|
260
|
+
ip_obj = ipaddress.ip_address(hostname)
|
|
261
|
+
except ValueError:
|
|
262
|
+
# Not an IP address, it's a hostname - caller should continue with DNS resolution
|
|
263
|
+
return False
|
|
264
|
+
|
|
265
|
+
# It's a direct IP address
|
|
266
|
+
# Check if IP is in allowlist
|
|
267
|
+
if is_host_allowed(hostname, str(ip_obj)):
|
|
268
|
+
logger.debug("IP address %s is in allowlist, bypassing SSRF checks", hostname)
|
|
269
|
+
return True
|
|
270
|
+
|
|
271
|
+
if is_ip_blocked(ip_obj):
|
|
272
|
+
msg = (
|
|
273
|
+
f"Access to IP address {hostname} is blocked by SSRF protection. "
|
|
274
|
+
"Requests to private/internal IP ranges are not allowed for security reasons. "
|
|
275
|
+
"To allow this IP, add it to LANGFLOW_SSRF_ALLOWED_HOSTS environment variable."
|
|
276
|
+
)
|
|
277
|
+
raise SSRFProtectionError(msg)
|
|
278
|
+
|
|
279
|
+
# Direct IP is allowed (public IP)
|
|
280
|
+
return True
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _validate_hostname_resolution(hostname: str) -> None:
|
|
284
|
+
"""Resolve hostname and validate resolved IPs are not blocked.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
hostname: Hostname to resolve and validate
|
|
288
|
+
|
|
289
|
+
Raises:
|
|
290
|
+
SSRFProtectionError: If resolved IPs are blocked
|
|
291
|
+
"""
|
|
292
|
+
# Resolve hostname to IP addresses
|
|
293
|
+
try:
|
|
294
|
+
resolved_ips = resolve_hostname(hostname)
|
|
295
|
+
except SSRFProtectionError:
|
|
296
|
+
# Re-raise SSRF errors as-is
|
|
297
|
+
raise
|
|
298
|
+
except Exception as e:
|
|
299
|
+
msg = f"Failed to resolve hostname {hostname}: {e}"
|
|
300
|
+
raise SSRFProtectionError(msg) from e
|
|
301
|
+
|
|
302
|
+
# Check if any resolved IP is blocked
|
|
303
|
+
blocked_ips = []
|
|
304
|
+
for ip in resolved_ips:
|
|
305
|
+
# Check if this specific IP is in the allowlist
|
|
306
|
+
if is_host_allowed(hostname, ip):
|
|
307
|
+
logger.debug("Resolved IP %s for hostname %s is in allowlist, bypassing SSRF checks", ip, hostname)
|
|
308
|
+
return
|
|
309
|
+
|
|
310
|
+
if is_ip_blocked(ip):
|
|
311
|
+
blocked_ips.append(ip)
|
|
312
|
+
|
|
313
|
+
if blocked_ips:
|
|
314
|
+
msg = (
|
|
315
|
+
f"Hostname {hostname} resolves to blocked IP address(es): {', '.join(blocked_ips)}. "
|
|
316
|
+
"Requests to private/internal IP ranges are not allowed for security reasons. "
|
|
317
|
+
"This protection prevents access to internal services, cloud metadata endpoints "
|
|
318
|
+
"(e.g., AWS 169.254.169.254), and other sensitive internal resources. "
|
|
319
|
+
"To allow this hostname, add it to LANGFLOW_SSRF_ALLOWED_HOSTS environment variable."
|
|
320
|
+
)
|
|
321
|
+
raise SSRFProtectionError(msg)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def validate_url_for_ssrf(url: str, *, warn_only: bool = True) -> None:
|
|
325
|
+
"""Validate a URL to prevent SSRF attacks.
|
|
326
|
+
|
|
327
|
+
This function performs the following checks:
|
|
328
|
+
1. Validates the URL scheme (only http/https allowed)
|
|
329
|
+
2. Validates hostname exists
|
|
330
|
+
3. Checks if hostname/IP is in allowlist
|
|
331
|
+
4. If direct IP: validates it's not in blocked ranges
|
|
332
|
+
5. If hostname: resolves to IPs and validates they're not in blocked ranges
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
url: URL to validate
|
|
336
|
+
warn_only: If True, only log warnings instead of raising errors (default: True)
|
|
337
|
+
TODO: Change default to False in next major version (2.0)
|
|
338
|
+
|
|
339
|
+
Raises:
|
|
340
|
+
SSRFProtectionError: If the URL is blocked due to SSRF protection (only if warn_only=False)
|
|
341
|
+
ValueError: If the URL is malformed
|
|
342
|
+
"""
|
|
343
|
+
# Skip validation if SSRF protection is disabled
|
|
344
|
+
if not is_ssrf_protection_enabled():
|
|
345
|
+
return
|
|
346
|
+
|
|
347
|
+
# Parse URL
|
|
348
|
+
try:
|
|
349
|
+
parsed = urlparse(url)
|
|
350
|
+
except Exception as e:
|
|
351
|
+
msg = f"Invalid URL format: {e}"
|
|
352
|
+
raise ValueError(msg) from e
|
|
353
|
+
|
|
354
|
+
try:
|
|
355
|
+
# Validate scheme
|
|
356
|
+
_validate_url_scheme(parsed.scheme)
|
|
357
|
+
if parsed.scheme not in ("http", "https"):
|
|
358
|
+
return
|
|
359
|
+
|
|
360
|
+
# Validate hostname exists
|
|
361
|
+
hostname = _validate_hostname_exists(parsed.hostname)
|
|
362
|
+
|
|
363
|
+
# Check if hostname/IP is in allowlist (early return if allowed)
|
|
364
|
+
if is_host_allowed(hostname):
|
|
365
|
+
logger.debug("Hostname %s is in allowlist, bypassing SSRF checks", hostname)
|
|
366
|
+
return
|
|
367
|
+
|
|
368
|
+
# Validate direct IP address or resolve hostname
|
|
369
|
+
is_direct_ip = _validate_direct_ip_address(hostname)
|
|
370
|
+
if is_direct_ip:
|
|
371
|
+
# Direct IP was handled (allowed or exception raised)
|
|
372
|
+
return
|
|
373
|
+
|
|
374
|
+
# Not a direct IP, resolve hostname and validate
|
|
375
|
+
_validate_hostname_resolution(hostname)
|
|
376
|
+
except SSRFProtectionError as e:
|
|
377
|
+
if warn_only:
|
|
378
|
+
logger.warning("SSRF Protection Warning: %s [URL: %s]", str(e), url)
|
|
379
|
+
logger.warning(
|
|
380
|
+
"This request will be blocked when SSRF protection is enforced in the next major version. "
|
|
381
|
+
"Please review your API Request components."
|
|
382
|
+
)
|
|
383
|
+
return
|
|
384
|
+
raise
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Cloud environment validation utilities.
|
|
2
|
+
|
|
3
|
+
This module contains validation functions for cloud-specific constraints,
|
|
4
|
+
such as disabling certain features when running in Astra cloud environment.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def raise_error_if_astra_cloud_disable_component(msg: str):
|
|
11
|
+
"""Validate that we're not in an Astra cloud environment and certain components/features need to be disabled.
|
|
12
|
+
|
|
13
|
+
Check if the environment variable ASTRA_CLOUD_DISABLE_COMPONENT is set to true.
|
|
14
|
+
IF it is, then we know we are in an Astra cloud environment and
|
|
15
|
+
that certain components or component-features need to be disabled.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
msg: The error message to raise if we're in an Astra cloud environment.
|
|
19
|
+
|
|
20
|
+
Raises:
|
|
21
|
+
ValueError: If running in an Astra cloud environment.
|
|
22
|
+
"""
|
|
23
|
+
if (
|
|
24
|
+
disable_component := os.getenv("ASTRA_CLOUD_DISABLE_COMPONENT", "false")
|
|
25
|
+
) and disable_component.lower().strip() == "true":
|
|
26
|
+
raise ValueError(msg)
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lfx-nightly
|
|
3
|
-
Version: 0.2.0.
|
|
3
|
+
Version: 0.2.0.dev41
|
|
4
4
|
Summary: Langflow Executor - A lightweight CLI tool for executing and serving Langflow AI flows
|
|
5
5
|
Author-email: Gabriel Luiz Freitas Almeida <gabriel@langflow.org>
|
|
6
6
|
Requires-Python: <3.14,>=3.10
|
|
7
7
|
Requires-Dist: aiofile<4.0.0,>=3.8.0
|
|
8
8
|
Requires-Dist: aiofiles<25.0.0,>=24.1.0
|
|
9
9
|
Requires-Dist: asyncer<1.0.0,>=0.0.8
|
|
10
|
-
Requires-Dist: cachetools
|
|
10
|
+
Requires-Dist: cachetools>=6.0.0
|
|
11
11
|
Requires-Dist: chardet<6.0.0,>=5.2.0
|
|
12
12
|
Requires-Dist: defusedxml<1.0.0,>=0.7.1
|
|
13
13
|
Requires-Dist: docstring-parser<1.0.0,>=0.16
|
|
14
14
|
Requires-Dist: emoji<3.0.0,>=2.14.1
|
|
15
15
|
Requires-Dist: fastapi<1.0.0,>=0.115.13
|
|
16
|
+
Requires-Dist: filelock>=3.20.0
|
|
16
17
|
Requires-Dist: httpx[http2]<1.0.0,>=0.24.0
|
|
17
18
|
Requires-Dist: json-repair<1.0.0,>=0.30.3
|
|
18
19
|
Requires-Dist: langchain-core<1.0.0,>=0.3.66
|
|
@@ -27,6 +28,7 @@ Requires-Dist: pillow<13.0.0,>=10.0.0
|
|
|
27
28
|
Requires-Dist: platformdirs<5.0.0,>=4.3.8
|
|
28
29
|
Requires-Dist: pydantic-settings<3.0.0,>=2.10.1
|
|
29
30
|
Requires-Dist: pydantic<3.0.0,>=2.0.0
|
|
31
|
+
Requires-Dist: pypdf>=5.1.0
|
|
30
32
|
Requires-Dist: python-dotenv<2.0.0,>=1.0.0
|
|
31
33
|
Requires-Dist: rich<14.0.0,>=13.0.0
|
|
32
34
|
Requires-Dist: structlog<26.0.0,>=25.4.0
|
|
@@ -198,6 +200,7 @@ Features:
|
|
|
198
200
|
- Creates an agent with OpenAI GPT model
|
|
199
201
|
- Provides web search tools via URLComponent
|
|
200
202
|
- Connects ChatInput → Agent → ChatOutput
|
|
203
|
+
- Uses async get_graph() function for proper async handling
|
|
201
204
|
|
|
202
205
|
Usage:
|
|
203
206
|
uv run lfx run simple_agent.py "How are you?"
|
|
@@ -211,27 +214,40 @@ from lfx import components as cp
|
|
|
211
214
|
from lfx.graph import Graph
|
|
212
215
|
from lfx.log.logger import LogConfig
|
|
213
216
|
|
|
214
|
-
log_config = LogConfig(
|
|
215
|
-
log_level="INFO",
|
|
216
|
-
log_file=Path("langflow.log"),
|
|
217
|
-
)
|
|
218
217
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
)
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
218
|
+
async def get_graph() -> Graph:
|
|
219
|
+
"""Create and return the graph with async component initialization.
|
|
220
|
+
|
|
221
|
+
This function properly handles async component initialization without
|
|
222
|
+
blocking the module loading process. The script loader will detect this
|
|
223
|
+
async function and handle it appropriately.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Graph: The configured graph with ChatInput → Agent → ChatOutput flow
|
|
227
|
+
"""
|
|
228
|
+
log_config = LogConfig(
|
|
229
|
+
log_level="INFO",
|
|
230
|
+
log_file=Path("langflow.log"),
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# Showcase the new flattened component access - no need for deep imports!
|
|
234
|
+
chat_input = cp.ChatInput()
|
|
235
|
+
agent = cp.AgentComponent()
|
|
236
|
+
|
|
237
|
+
# Use URLComponent for web search capabilities
|
|
238
|
+
url_component = cp.URLComponent()
|
|
239
|
+
tools = await url_component.to_toolkit()
|
|
240
|
+
|
|
241
|
+
agent.set(
|
|
242
|
+
model_name="gpt-4.1-mini",
|
|
243
|
+
agent_llm="OpenAI",
|
|
244
|
+
api_key=os.getenv("OPENAI_API_KEY"),
|
|
245
|
+
input_value=chat_input.message_response,
|
|
246
|
+
tools=tools,
|
|
247
|
+
)
|
|
248
|
+
chat_output = cp.ChatOutput().set(input_value=agent.message_response)
|
|
249
|
+
|
|
250
|
+
return Graph(chat_input, chat_output, log_config=log_config)
|
|
235
251
|
```
|
|
236
252
|
|
|
237
253
|
**Step 2: Install dependencies**
|