zerochat 9.2.0__tar.gz → 9.3.0__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.
Potentially problematic release.
This version of zerochat might be problematic. Click here for more details.
- {zerochat-9.2.0/zerochat.egg-info → zerochat-9.3.0}/PKG-INFO +1 -1
- {zerochat-9.2.0 → zerochat-9.3.0}/pyproject.toml +1 -1
- {zerochat-9.2.0 → zerochat-9.3.0/zerochat.egg-info}/PKG-INFO +1 -1
- {zerochat-9.2.0 → zerochat-9.3.0}/zerochat.py +192 -4
- {zerochat-9.2.0 → zerochat-9.3.0}/LICENSE +0 -0
- {zerochat-9.2.0 → zerochat-9.3.0}/README.md +0 -0
- {zerochat-9.2.0 → zerochat-9.3.0}/setup.cfg +0 -0
- {zerochat-9.2.0 → zerochat-9.3.0}/zerochat.egg-info/SOURCES.txt +0 -0
- {zerochat-9.2.0 → zerochat-9.3.0}/zerochat.egg-info/dependency_links.txt +0 -0
- {zerochat-9.2.0 → zerochat-9.3.0}/zerochat.egg-info/entry_points.txt +0 -0
- {zerochat-9.2.0 → zerochat-9.3.0}/zerochat.egg-info/top_level.txt +0 -0
|
@@ -17,6 +17,7 @@ import base64
|
|
|
17
17
|
import datetime
|
|
18
18
|
import fnmatch
|
|
19
19
|
import functools
|
|
20
|
+
import gzip
|
|
20
21
|
import hashlib
|
|
21
22
|
import hmac
|
|
22
23
|
import importlib.metadata
|
|
@@ -36,14 +37,16 @@ import threading
|
|
|
36
37
|
import time
|
|
37
38
|
import traceback
|
|
38
39
|
import urllib.error
|
|
40
|
+
import urllib.parse
|
|
39
41
|
import urllib.request
|
|
40
42
|
from urllib.parse import urlencode
|
|
41
43
|
import venv
|
|
42
44
|
import webbrowser
|
|
45
|
+
import zlib
|
|
43
46
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
44
47
|
from pathlib import Path
|
|
45
48
|
|
|
46
|
-
SOURCE_BACKEND_VERSION = "9.
|
|
49
|
+
SOURCE_BACKEND_VERSION = "9.3.0"
|
|
47
50
|
|
|
48
51
|
def _read_source_version(filename: str) -> str | None:
|
|
49
52
|
"""Lee la versión de un archivo del repositorio cuando se ejecuta desde fuentes."""
|
|
@@ -1350,9 +1353,11 @@ def search_files(query: str, path: str = ".", file_pattern: str = None, max_resu
|
|
|
1350
1353
|
if not target.is_dir():
|
|
1351
1354
|
return _tool_error(f"Path '{path}' is not a directory.")
|
|
1352
1355
|
|
|
1356
|
+
warning = None
|
|
1353
1357
|
try:
|
|
1354
1358
|
regex = re.compile(query, re.MULTILINE)
|
|
1355
|
-
except re.error:
|
|
1359
|
+
except re.error as err:
|
|
1360
|
+
warning = f"Query '{query}' is not a valid regular expression ({err}); treated as literal text."
|
|
1356
1361
|
regex = re.compile(re.escape(query), re.MULTILINE)
|
|
1357
1362
|
|
|
1358
1363
|
try:
|
|
@@ -1412,7 +1417,7 @@ def search_files(query: str, path: str = ".", file_pattern: str = None, max_resu
|
|
|
1412
1417
|
if truncated:
|
|
1413
1418
|
break
|
|
1414
1419
|
|
|
1415
|
-
|
|
1420
|
+
payload = {
|
|
1416
1421
|
"success": True,
|
|
1417
1422
|
"path": str(target),
|
|
1418
1423
|
"query": query,
|
|
@@ -1421,7 +1426,11 @@ def search_files(query: str, path: str = ".", file_pattern: str = None, max_resu
|
|
|
1421
1426
|
"total_matches": len(matches),
|
|
1422
1427
|
"truncated": truncated,
|
|
1423
1428
|
"matches": matches
|
|
1424
|
-
}
|
|
1429
|
+
}
|
|
1430
|
+
if warning:
|
|
1431
|
+
payload["warning"] = warning
|
|
1432
|
+
|
|
1433
|
+
return json.dumps(payload, ensure_ascii=False, indent=2)
|
|
1425
1434
|
except Exception as e:
|
|
1426
1435
|
return _tool_error(str(e))
|
|
1427
1436
|
|
|
@@ -2700,6 +2709,178 @@ DEV_CONTENT_TYPES = {
|
|
|
2700
2709
|
".map": "application/json",
|
|
2701
2710
|
}
|
|
2702
2711
|
|
|
2712
|
+
METADATA_HOSTS = frozenset({
|
|
2713
|
+
"169.254.169.254",
|
|
2714
|
+
"metadata.google.internal",
|
|
2715
|
+
"instance-data",
|
|
2716
|
+
"100.100.100.200",
|
|
2717
|
+
})
|
|
2718
|
+
DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
|
2719
|
+
|
|
2720
|
+
|
|
2721
|
+
def is_dangerous_host(hostname: str, allow_local: bool = False) -> bool:
|
|
2722
|
+
"""Verifica si un host representa un riesgo de SSRF (metadatos cloud, loopback o privados)."""
|
|
2723
|
+
if not hostname:
|
|
2724
|
+
return True
|
|
2725
|
+
clean_host = hostname.strip().lower().strip("[]")
|
|
2726
|
+
if clean_host in METADATA_HOSTS:
|
|
2727
|
+
return True
|
|
2728
|
+
|
|
2729
|
+
try:
|
|
2730
|
+
ip = ipaddress.ip_address(clean_host)
|
|
2731
|
+
if ip.is_link_local:
|
|
2732
|
+
return True
|
|
2733
|
+
if not allow_local and (ip.is_loopback or ip.is_private or ip.is_reserved or ip.is_unspecified):
|
|
2734
|
+
return True
|
|
2735
|
+
return False
|
|
2736
|
+
except ValueError:
|
|
2737
|
+
pass
|
|
2738
|
+
|
|
2739
|
+
if clean_host.isdigit():
|
|
2740
|
+
try:
|
|
2741
|
+
num = int(clean_host)
|
|
2742
|
+
if 0 <= num <= 4294967295:
|
|
2743
|
+
ip = ipaddress.IPv4Address(num)
|
|
2744
|
+
if ip.is_link_local:
|
|
2745
|
+
return True
|
|
2746
|
+
if not allow_local and (ip.is_loopback or ip.is_private or ip.is_reserved or ip.is_unspecified):
|
|
2747
|
+
return True
|
|
2748
|
+
return False
|
|
2749
|
+
except ValueError:
|
|
2750
|
+
pass
|
|
2751
|
+
|
|
2752
|
+
if clean_host.startswith("0x"):
|
|
2753
|
+
try:
|
|
2754
|
+
num = int(clean_host, 16)
|
|
2755
|
+
if 0 <= num <= 4294967295:
|
|
2756
|
+
ip = ipaddress.IPv4Address(num)
|
|
2757
|
+
if ip.is_link_local:
|
|
2758
|
+
return True
|
|
2759
|
+
if not allow_local and (ip.is_loopback or ip.is_private or ip.is_reserved or ip.is_unspecified):
|
|
2760
|
+
return True
|
|
2761
|
+
return False
|
|
2762
|
+
except ValueError:
|
|
2763
|
+
pass
|
|
2764
|
+
|
|
2765
|
+
if not allow_local:
|
|
2766
|
+
if clean_host in ("localhost", "0.0.0.0", "127.0.0.1", "::1"):
|
|
2767
|
+
return True
|
|
2768
|
+
if clean_host.endswith(".local") or clean_host.endswith(".internal"):
|
|
2769
|
+
return True
|
|
2770
|
+
|
|
2771
|
+
return False
|
|
2772
|
+
|
|
2773
|
+
|
|
2774
|
+
def fetch_web_resource(params: dict) -> dict:
|
|
2775
|
+
"""Descarga de forma segura un recurso web vía HTTP/HTTPS para el cliente web."""
|
|
2776
|
+
url = params.get("url")
|
|
2777
|
+
if not isinstance(url, str) or not url.strip():
|
|
2778
|
+
raise ValueError("URL no válida o ausente.")
|
|
2779
|
+
url = url.strip()
|
|
2780
|
+
parsed = urllib.parse.urlparse(url)
|
|
2781
|
+
if parsed.scheme not in ("http", "https"):
|
|
2782
|
+
raise ValueError(f"Protocolo no permitido: '{parsed.scheme}'. Solo se admite http y https.")
|
|
2783
|
+
hostname = (parsed.hostname or "").lower().strip("[]")
|
|
2784
|
+
if not hostname:
|
|
2785
|
+
raise ValueError("URL sin nombre de host válido.")
|
|
2786
|
+
|
|
2787
|
+
allow_local = bool(params.get("allow_local", False))
|
|
2788
|
+
if is_dangerous_host(hostname, allow_local):
|
|
2789
|
+
raise PermissionError("Acceso bloqueado: intento de acceso a servicios internos o metadatos de infraestructura (SSRF).")
|
|
2790
|
+
|
|
2791
|
+
timeout_s = float(params.get("timeout_s", 10.0))
|
|
2792
|
+
timeout_s = max(0.5, min(timeout_s, 30.0))
|
|
2793
|
+
max_bytes = int(params.get("max_bytes", 2 * 1024 * 1024))
|
|
2794
|
+
max_bytes = max(1024, min(max_bytes, 10 * 1024 * 1024))
|
|
2795
|
+
as_binary = bool(params.get("binary", False))
|
|
2796
|
+
|
|
2797
|
+
req_headers = {
|
|
2798
|
+
"User-Agent": DEFAULT_USER_AGENT,
|
|
2799
|
+
"Accept": "text/html,application/xhtml+xml,application/json,application/pdf,text/plain;q=0.9,*/*;q=0.8",
|
|
2800
|
+
"Accept-Encoding": "gzip, deflate",
|
|
2801
|
+
}
|
|
2802
|
+
custom_headers = params.get("headers")
|
|
2803
|
+
if isinstance(custom_headers, dict):
|
|
2804
|
+
for k, v in custom_headers.items():
|
|
2805
|
+
if isinstance(k, str) and isinstance(v, str) and k.lower() != "host":
|
|
2806
|
+
req_headers[k] = v
|
|
2807
|
+
|
|
2808
|
+
req = urllib.request.Request(url, headers=req_headers)
|
|
2809
|
+
try:
|
|
2810
|
+
with urllib.request.urlopen(req, timeout=timeout_s) as resp:
|
|
2811
|
+
status = resp.status
|
|
2812
|
+
content_type = resp.headers.get("Content-Type", "")
|
|
2813
|
+
content_encoding = resp.headers.get("Content-Encoding", "").lower()
|
|
2814
|
+
raw_data = resp.read(max_bytes + 1)
|
|
2815
|
+
truncated = len(raw_data) > max_bytes
|
|
2816
|
+
if truncated:
|
|
2817
|
+
raw_data = raw_data[:max_bytes]
|
|
2818
|
+
|
|
2819
|
+
if "gzip" in content_encoding:
|
|
2820
|
+
try:
|
|
2821
|
+
raw_data = gzip.decompress(raw_data)
|
|
2822
|
+
except (OSError, gzip.BadGzipFile):
|
|
2823
|
+
pass
|
|
2824
|
+
elif "deflate" in content_encoding:
|
|
2825
|
+
try:
|
|
2826
|
+
raw_data = zlib.decompress(raw_data)
|
|
2827
|
+
except (OSError, zlib.error):
|
|
2828
|
+
pass
|
|
2829
|
+
|
|
2830
|
+
resp_headers = {k: v for k, v in resp.getheaders()}
|
|
2831
|
+
|
|
2832
|
+
if as_binary:
|
|
2833
|
+
return {
|
|
2834
|
+
"ok": True,
|
|
2835
|
+
"status": status,
|
|
2836
|
+
"content_type": content_type,
|
|
2837
|
+
"headers": resp_headers,
|
|
2838
|
+
"base64": base64.b64encode(raw_data).decode("ascii"),
|
|
2839
|
+
"byte_size": len(raw_data),
|
|
2840
|
+
"truncated": truncated
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2843
|
+
charset = resp.headers.get_content_charset() or "utf-8"
|
|
2844
|
+
try:
|
|
2845
|
+
text_content = raw_data.decode(charset, errors="replace")
|
|
2846
|
+
return {
|
|
2847
|
+
"ok": True,
|
|
2848
|
+
"status": status,
|
|
2849
|
+
"content_type": content_type,
|
|
2850
|
+
"headers": resp_headers,
|
|
2851
|
+
"text": text_content,
|
|
2852
|
+
"byte_size": len(raw_data),
|
|
2853
|
+
"truncated": truncated
|
|
2854
|
+
}
|
|
2855
|
+
except (UnicodeDecodeError, LookupError):
|
|
2856
|
+
return {
|
|
2857
|
+
"ok": True,
|
|
2858
|
+
"status": status,
|
|
2859
|
+
"content_type": content_type,
|
|
2860
|
+
"headers": resp_headers,
|
|
2861
|
+
"base64": base64.b64encode(raw_data).decode("ascii"),
|
|
2862
|
+
"byte_size": len(raw_data),
|
|
2863
|
+
"truncated": truncated
|
|
2864
|
+
}
|
|
2865
|
+
except urllib.error.HTTPError as he:
|
|
2866
|
+
err_body = ""
|
|
2867
|
+
try:
|
|
2868
|
+
err_body = he.read(2048).decode("utf-8", errors="replace")
|
|
2869
|
+
except (OSError, UnicodeDecodeError):
|
|
2870
|
+
pass
|
|
2871
|
+
return {
|
|
2872
|
+
"ok": False,
|
|
2873
|
+
"status": he.code,
|
|
2874
|
+
"error": f"HTTP {he.code}: {he.reason}",
|
|
2875
|
+
"text": err_body
|
|
2876
|
+
}
|
|
2877
|
+
except Exception as exc:
|
|
2878
|
+
return {
|
|
2879
|
+
"ok": False,
|
|
2880
|
+
"status": 0,
|
|
2881
|
+
"error": str(exc)
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2703
2884
|
|
|
2704
2885
|
class ZeroChatServerHandler(BaseHTTPRequestHandler):
|
|
2705
2886
|
server_version = f"ZeroChatServer/{VERSION}"
|
|
@@ -3105,6 +3286,13 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
|
|
|
3105
3286
|
return set_project_directory(params.get("path")), None, ""
|
|
3106
3287
|
except (ValueError, OSError) as exc:
|
|
3107
3288
|
return None, {"code": -32602, "message": str(exc)}, ""
|
|
3289
|
+
if method == "zerochat/web/fetch":
|
|
3290
|
+
try:
|
|
3291
|
+
return fetch_web_resource(params), None, ""
|
|
3292
|
+
except (ValueError, PermissionError) as exc:
|
|
3293
|
+
return None, {"code": -32602, "message": str(exc)}, ""
|
|
3294
|
+
except Exception as exc:
|
|
3295
|
+
return None, {"code": -32603, "message": f"Error en transporte web: {exc}"}, str(exc)
|
|
3108
3296
|
if method.startswith("zerochat/jobs/"):
|
|
3109
3297
|
return self._dispatch_jobs(method, params)
|
|
3110
3298
|
if method == "zerochat/external/status":
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|