sf-veritas 0.11.10__cp314-cp314-manylinux_2_28_x86_64.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.
- sf_veritas/__init__.py +46 -0
- sf_veritas/_auto_preload.py +73 -0
- sf_veritas/_sfconfig.c +162 -0
- sf_veritas/_sfconfig.cpython-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfcrashhandler.c +267 -0
- sf_veritas/_sfcrashhandler.cpython-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffastlog.c +953 -0
- sf_veritas/_sffastlog.cpython-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffastnet.c +994 -0
- sf_veritas/_sffastnet.cpython-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffastnetworkrequest.c +727 -0
- sf_veritas/_sffastnetworkrequest.cpython-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffuncspan.c +2791 -0
- sf_veritas/_sffuncspan.cpython-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffuncspan_config.c +730 -0
- sf_veritas/_sffuncspan_config.cpython-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfheadercheck.c +341 -0
- sf_veritas/_sfheadercheck.cpython-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfnetworkhop.c +1454 -0
- sf_veritas/_sfnetworkhop.cpython-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfservice.c +1223 -0
- sf_veritas/_sfservice.cpython-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfteepreload.c +6227 -0
- sf_veritas/app_config.py +57 -0
- sf_veritas/cli.py +336 -0
- sf_veritas/constants.py +10 -0
- sf_veritas/custom_excepthook.py +304 -0
- sf_veritas/custom_log_handler.py +146 -0
- sf_veritas/custom_output_wrapper.py +153 -0
- sf_veritas/custom_print.py +153 -0
- sf_veritas/django_app.py +5 -0
- sf_veritas/env_vars.py +186 -0
- sf_veritas/exception_handling_middleware.py +18 -0
- sf_veritas/exception_metaclass.py +69 -0
- sf_veritas/fast_frame_info.py +116 -0
- sf_veritas/fast_network_hop.py +293 -0
- sf_veritas/frame_tools.py +112 -0
- sf_veritas/funcspan_config_loader.py +693 -0
- sf_veritas/function_span_profiler.py +1313 -0
- sf_veritas/get_preload_path.py +34 -0
- sf_veritas/import_hook.py +62 -0
- sf_veritas/infra_details/__init__.py +3 -0
- sf_veritas/infra_details/get_infra_details.py +24 -0
- sf_veritas/infra_details/kubernetes/__init__.py +3 -0
- sf_veritas/infra_details/kubernetes/get_cluster_name.py +147 -0
- sf_veritas/infra_details/kubernetes/get_details.py +7 -0
- sf_veritas/infra_details/running_on/__init__.py +17 -0
- sf_veritas/infra_details/running_on/kubernetes.py +11 -0
- sf_veritas/interceptors.py +543 -0
- sf_veritas/libsfnettee.so +0 -0
- sf_veritas/local_env_detect.py +118 -0
- sf_veritas/package_metadata.py +6 -0
- sf_veritas/patches/__init__.py +0 -0
- sf_veritas/patches/_patch_tracker.py +74 -0
- sf_veritas/patches/concurrent_futures.py +19 -0
- sf_veritas/patches/constants.py +1 -0
- sf_veritas/patches/exceptions.py +82 -0
- sf_veritas/patches/multiprocessing.py +32 -0
- sf_veritas/patches/network_libraries/__init__.py +99 -0
- sf_veritas/patches/network_libraries/aiohttp.py +294 -0
- sf_veritas/patches/network_libraries/curl_cffi.py +363 -0
- sf_veritas/patches/network_libraries/http_client.py +670 -0
- sf_veritas/patches/network_libraries/httpcore.py +580 -0
- sf_veritas/patches/network_libraries/httplib2.py +315 -0
- sf_veritas/patches/network_libraries/httpx.py +557 -0
- sf_veritas/patches/network_libraries/niquests.py +218 -0
- sf_veritas/patches/network_libraries/pycurl.py +399 -0
- sf_veritas/patches/network_libraries/requests.py +595 -0
- sf_veritas/patches/network_libraries/ssl_socket.py +822 -0
- sf_veritas/patches/network_libraries/tornado.py +360 -0
- sf_veritas/patches/network_libraries/treq.py +270 -0
- sf_veritas/patches/network_libraries/urllib_request.py +483 -0
- sf_veritas/patches/network_libraries/utils.py +598 -0
- sf_veritas/patches/os.py +17 -0
- sf_veritas/patches/threading.py +231 -0
- sf_veritas/patches/web_frameworks/__init__.py +54 -0
- sf_veritas/patches/web_frameworks/aiohttp.py +798 -0
- sf_veritas/patches/web_frameworks/async_websocket_consumer.py +337 -0
- sf_veritas/patches/web_frameworks/blacksheep.py +532 -0
- sf_veritas/patches/web_frameworks/bottle.py +513 -0
- sf_veritas/patches/web_frameworks/cherrypy.py +683 -0
- sf_veritas/patches/web_frameworks/cors_utils.py +122 -0
- sf_veritas/patches/web_frameworks/django.py +963 -0
- sf_veritas/patches/web_frameworks/eve.py +401 -0
- sf_veritas/patches/web_frameworks/falcon.py +931 -0
- sf_veritas/patches/web_frameworks/fastapi.py +738 -0
- sf_veritas/patches/web_frameworks/flask.py +526 -0
- sf_veritas/patches/web_frameworks/klein.py +501 -0
- sf_veritas/patches/web_frameworks/litestar.py +616 -0
- sf_veritas/patches/web_frameworks/pyramid.py +440 -0
- sf_veritas/patches/web_frameworks/quart.py +841 -0
- sf_veritas/patches/web_frameworks/robyn.py +708 -0
- sf_veritas/patches/web_frameworks/sanic.py +874 -0
- sf_veritas/patches/web_frameworks/starlette.py +742 -0
- sf_veritas/patches/web_frameworks/strawberry.py +1446 -0
- sf_veritas/patches/web_frameworks/tornado.py +485 -0
- sf_veritas/patches/web_frameworks/utils.py +170 -0
- sf_veritas/print_override.py +13 -0
- sf_veritas/regular_data_transmitter.py +444 -0
- sf_veritas/request_interceptor.py +401 -0
- sf_veritas/request_utils.py +550 -0
- sf_veritas/segfault_handler.py +116 -0
- sf_veritas/server_status.py +1 -0
- sf_veritas/shutdown_flag.py +11 -0
- sf_veritas/subprocess_startup.py +3 -0
- sf_veritas/test_cli.py +145 -0
- sf_veritas/thread_local.py +1319 -0
- sf_veritas/timeutil.py +114 -0
- sf_veritas/transmit_exception_to_sailfish.py +28 -0
- sf_veritas/transmitter.py +132 -0
- sf_veritas/types.py +47 -0
- sf_veritas/unified_interceptor.py +1678 -0
- sf_veritas/utils.py +39 -0
- sf_veritas-0.11.10.dist-info/METADATA +97 -0
- sf_veritas-0.11.10.dist-info/RECORD +141 -0
- sf_veritas-0.11.10.dist-info/WHEEL +5 -0
- sf_veritas-0.11.10.dist-info/entry_points.txt +2 -0
- sf_veritas-0.11.10.dist-info/top_level.txt +1 -0
- sf_veritas.libs/libbrotlicommon-6ce2a53c.so.1.0.6 +0 -0
- sf_veritas.libs/libbrotlidec-811d1be3.so.1.0.6 +0 -0
- sf_veritas.libs/libcom_err-730ca923.so.2.1 +0 -0
- sf_veritas.libs/libcrypt-52aca757.so.1.1.0 +0 -0
- sf_veritas.libs/libcrypto-bdaed0ea.so.1.1.1k +0 -0
- sf_veritas.libs/libcurl-eaa3cf66.so.4.5.0 +0 -0
- sf_veritas.libs/libgssapi_krb5-323bbd21.so.2.2 +0 -0
- sf_veritas.libs/libidn2-2f4a5893.so.0.3.6 +0 -0
- sf_veritas.libs/libk5crypto-9a74ff38.so.3.1 +0 -0
- sf_veritas.libs/libkeyutils-2777d33d.so.1.6 +0 -0
- sf_veritas.libs/libkrb5-a55300e8.so.3.3 +0 -0
- sf_veritas.libs/libkrb5support-e6594cfc.so.0.1 +0 -0
- sf_veritas.libs/liblber-2-d20824ef.4.so.2.10.9 +0 -0
- sf_veritas.libs/libldap-2-cea2a960.4.so.2.10.9 +0 -0
- sf_veritas.libs/libnghttp2-39367a22.so.14.17.0 +0 -0
- sf_veritas.libs/libpcre2-8-516f4c9d.so.0.7.1 +0 -0
- sf_veritas.libs/libpsl-99becdd3.so.5.3.1 +0 -0
- sf_veritas.libs/libsasl2-7de4d792.so.3.0.0 +0 -0
- sf_veritas.libs/libselinux-d0805dcb.so.1 +0 -0
- sf_veritas.libs/libssh-c11d285b.so.4.8.7 +0 -0
- sf_veritas.libs/libssl-60250281.so.1.1.1k +0 -0
- sf_veritas.libs/libunistring-05abdd40.so.2.1.0 +0 -0
- sf_veritas.libs/libuuid-95b83d40.so.1.3.0 +0 -0
|
@@ -0,0 +1,822 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ultra-fast SSL socket tee for HTTPS traffic capture.
|
|
3
|
+
~15-20ns overhead per recv/send operation.
|
|
4
|
+
|
|
5
|
+
CRITICAL: This must be patched FIRST before any other HTTP library patches,
|
|
6
|
+
because requests/httpx/urllib3/aiohttp all use ssl.SSLSocket underneath.
|
|
7
|
+
|
|
8
|
+
Architecture:
|
|
9
|
+
1. Hot path (~15-20ns): Tee data to thread-local deque, return immediately
|
|
10
|
+
2. Background thread: Aggregates bytes → HTTP transactions
|
|
11
|
+
3. Push complete transactions to C ring buffer via ctypes
|
|
12
|
+
4. C ring handles queueing, retries, HTTP/2, telemetry delivery
|
|
13
|
+
|
|
14
|
+
Performance:
|
|
15
|
+
- Thread-local deque: No locks, ~10ns append
|
|
16
|
+
- Graceful degradation: Drop if queue full, never block caller
|
|
17
|
+
- Zero hot path impact: All parsing/transmission happens in background
|
|
18
|
+
|
|
19
|
+
**IMPORTANT: This code is currently DISABLED by default.**
|
|
20
|
+
**Set SF_ENABLE_PYTHON_SSL_TEE=1 to enable.**
|
|
21
|
+
**By default, SF_SSL_PYTHON_MODE=1 just disables C SSL hooks without Python capture.**
|
|
22
|
+
|
|
23
|
+
Environment Variables (respects SF_NETWORKREQUEST_CAPTURE_* settings):
|
|
24
|
+
- SF_NETWORKREQUEST_CAPTURE_ENABLED: Enable/disable all capture (default: true)
|
|
25
|
+
- SF_NETWORKREQUEST_CAPTURE_REQUEST_HEADERS: Capture request headers (default: true)
|
|
26
|
+
- SF_NETWORKREQUEST_CAPTURE_REQUEST_BODY: Capture request body (default: true)
|
|
27
|
+
- SF_NETWORKREQUEST_CAPTURE_RESPONSE_HEADERS: Capture response headers (default: true)
|
|
28
|
+
- SF_NETWORKREQUEST_CAPTURE_RESPONSE_BODY: Capture response body (default: true)
|
|
29
|
+
- SF_NETWORKREQUEST_REQUEST_LIMIT_MB: Max request body size in MB (default: 1)
|
|
30
|
+
- SF_NETWORKREQUEST_RESPONSE_LIMIT_MB: Max response body size in MB (default: 1)
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
import ssl
|
|
34
|
+
import socket
|
|
35
|
+
import threading
|
|
36
|
+
import time
|
|
37
|
+
import ctypes
|
|
38
|
+
from collections import deque
|
|
39
|
+
from typing import Optional, Dict, Any
|
|
40
|
+
import os
|
|
41
|
+
|
|
42
|
+
# Import record_network_request for collectNetworkRequest mutation
|
|
43
|
+
from .utils import record_network_request, init_fast_network_tracking
|
|
44
|
+
|
|
45
|
+
# ============================================================================
|
|
46
|
+
# CONFIGURATION
|
|
47
|
+
# ============================================================================
|
|
48
|
+
|
|
49
|
+
# Check if Python SSL tee is enabled (DISABLED by default)
|
|
50
|
+
_PYTHON_SSL_TEE_ENABLED = os.getenv('SF_ENABLE_PYTHON_SSL_TEE', 'false').lower() == 'true'
|
|
51
|
+
_C_RING_AVAILABLE = os.getenv('SF_SSL_PYTHON_MODE', '1') == '1' and _PYTHON_SSL_TEE_ENABLED
|
|
52
|
+
_SF_DEBUG = os.getenv('SF_DEBUG', 'false').lower() == 'true'
|
|
53
|
+
|
|
54
|
+
# SF_NETWORKREQUEST_CAPTURE_* environment variables (for collectNetworkRequest mutations)
|
|
55
|
+
# These control what data is captured and sent to the C ring
|
|
56
|
+
_CAPTURE_ENABLED = os.getenv('SF_NETWORKREQUEST_CAPTURE_ENABLED', 'true').lower() == 'true'
|
|
57
|
+
_CAPTURE_REQUEST_HEADERS = os.getenv('SF_NETWORKREQUEST_CAPTURE_REQUEST_HEADERS', 'true').lower() == 'true'
|
|
58
|
+
_CAPTURE_REQUEST_BODY = os.getenv('SF_NETWORKREQUEST_CAPTURE_REQUEST_BODY', 'true').lower() == 'true'
|
|
59
|
+
_CAPTURE_RESPONSE_HEADERS = os.getenv('SF_NETWORKREQUEST_CAPTURE_RESPONSE_HEADERS', 'true').lower() == 'true'
|
|
60
|
+
_CAPTURE_RESPONSE_BODY = os.getenv('SF_NETWORKREQUEST_CAPTURE_RESPONSE_BODY', 'true').lower() == 'true'
|
|
61
|
+
|
|
62
|
+
# Size limits (MB to bytes conversion)
|
|
63
|
+
_REQUEST_LIMIT_MB = float(os.getenv('SF_NETWORKREQUEST_REQUEST_LIMIT_MB', '1'))
|
|
64
|
+
_RESPONSE_LIMIT_MB = float(os.getenv('SF_NETWORKREQUEST_RESPONSE_LIMIT_MB', '1'))
|
|
65
|
+
MAX_REQUEST_BODY_CAPTURE = int(_REQUEST_LIMIT_MB * 1024 * 1024)
|
|
66
|
+
MAX_RESPONSE_BODY_CAPTURE = int(_RESPONSE_LIMIT_MB * 1024 * 1024)
|
|
67
|
+
|
|
68
|
+
# ============================================================================
|
|
69
|
+
# THREAD-LOCAL CAPTURE QUEUES (Zero-lock, ~10ns append)
|
|
70
|
+
# ============================================================================
|
|
71
|
+
|
|
72
|
+
_capture_queues = {} # thread_id -> deque
|
|
73
|
+
_capture_queues_lock = threading.Lock()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _get_capture_queue():
|
|
77
|
+
"""Get thread-local capture queue (~5ns lookup)"""
|
|
78
|
+
tid = threading.get_ident()
|
|
79
|
+
if tid not in _capture_queues:
|
|
80
|
+
with _capture_queues_lock:
|
|
81
|
+
if tid not in _capture_queues:
|
|
82
|
+
_capture_queues[tid] = deque(maxlen=1000) # Bounded to prevent OOM
|
|
83
|
+
return _capture_queues[tid]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ============================================================================
|
|
87
|
+
# HOT PATH - ULTRA FAST (~15-20ns overhead)
|
|
88
|
+
# ============================================================================
|
|
89
|
+
|
|
90
|
+
def _tee_capture(sock, data, direction):
|
|
91
|
+
"""
|
|
92
|
+
Tee-style capture: ~15ns overhead, non-blocking
|
|
93
|
+
|
|
94
|
+
Recursion Prevention:
|
|
95
|
+
- Detects X-Sf3-TelemetryOutbound header in requests (telemetry marker)
|
|
96
|
+
- Marks socket as telemetry and skips all future captures
|
|
97
|
+
- Prevents infinite recursion from capturing our own telemetry traffic
|
|
98
|
+
|
|
99
|
+
Performance breakdown:
|
|
100
|
+
- Get thread-local queue: ~5ns
|
|
101
|
+
- Append to deque: ~5-10ns
|
|
102
|
+
- Wake background thread: ~5ns
|
|
103
|
+
Total: ~15-20ns
|
|
104
|
+
"""
|
|
105
|
+
# Defensive: ensure data is bytes-like before processing
|
|
106
|
+
if not data:
|
|
107
|
+
return
|
|
108
|
+
if not isinstance(data, (bytes, bytearray)):
|
|
109
|
+
# Invalid data type (should never happen, but be defensive)
|
|
110
|
+
return
|
|
111
|
+
if len(data) == 0:
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
# CRITICAL: Recursion prevention - detect telemetry traffic by X-Sf3-TelemetryOutbound header
|
|
115
|
+
# This header is ONLY added to our own telemetry requests (in request_utils.py)
|
|
116
|
+
# Normal application traffic (even to echo endpoints) will NOT have this header
|
|
117
|
+
if b'X-Sf3-TelemetryOutbound:' in data or b'x-sf3-telemetryoutbound:' in data:
|
|
118
|
+
# Mark this socket as telemetry to skip all future captures
|
|
119
|
+
sock._is_telemetry_socket = True
|
|
120
|
+
if _SF_DEBUG:
|
|
121
|
+
print(f"[ssl_socket.py] _tee_capture: Detected telemetry socket, marking to skip", log=False)
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
# Skip if already marked as telemetry socket
|
|
125
|
+
if getattr(sock, '_is_telemetry_socket', False):
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
queue = _get_capture_queue()
|
|
130
|
+
sock_id = id(sock)
|
|
131
|
+
|
|
132
|
+
if direction == 'TX' and _SF_DEBUG:
|
|
133
|
+
chunk = bytes(data)
|
|
134
|
+
existing = getattr(sock, '_sf_hdr_buf', b'')
|
|
135
|
+
combined = existing + chunk
|
|
136
|
+
if b'\r\n\r\n' in combined and not getattr(sock, '_sf_headers_logged', False):
|
|
137
|
+
header_bytes = combined.split(b'\r\n\r\n', 1)[0]
|
|
138
|
+
try:
|
|
139
|
+
header_text = header_bytes.decode('latin-1', errors='ignore')
|
|
140
|
+
except Exception:
|
|
141
|
+
header_text = "<failed to decode headers>"
|
|
142
|
+
print(f"[ssl_socket.py] PRE-SEND HEADERS (sock_id={sock_id}):\n{header_text}", log=False)
|
|
143
|
+
sock._sf_headers_logged = True
|
|
144
|
+
sock._sf_hdr_buf = b''
|
|
145
|
+
else:
|
|
146
|
+
sock._sf_hdr_buf = combined[-8192:] # keep manageable buffer
|
|
147
|
+
|
|
148
|
+
# Debug: Log first capture for each socket
|
|
149
|
+
if _SF_DEBUG and not hasattr(sock, '_first_capture_logged'):
|
|
150
|
+
sock._first_capture_logged = True
|
|
151
|
+
peername = getattr(sock, '_peername_cache', None)
|
|
152
|
+
print(f"[ssl_socket.py] _tee_capture: FIRST CAPTURE sock_id={sock_id}, direction={direction}, size={len(data)} bytes, peername={peername}", log=False)
|
|
153
|
+
|
|
154
|
+
queue.append({
|
|
155
|
+
'sock_id': sock_id,
|
|
156
|
+
'peername': getattr(sock, '_peername_cache', None),
|
|
157
|
+
'data': data,
|
|
158
|
+
'direction': direction,
|
|
159
|
+
'timestamp': time.perf_counter_ns()
|
|
160
|
+
})
|
|
161
|
+
_background_event.set() # Wake background processor (non-blocking)
|
|
162
|
+
except Exception as e:
|
|
163
|
+
# Never let capture errors break the hot path
|
|
164
|
+
if _SF_DEBUG:
|
|
165
|
+
print(f"[ssl_socket.py] _tee_capture: Exception: {e}", log=False)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# ============================================================================
|
|
169
|
+
# SSL SOCKET MONKEY PATCHING
|
|
170
|
+
# ============================================================================
|
|
171
|
+
|
|
172
|
+
_original_recv = None
|
|
173
|
+
_original_send = None
|
|
174
|
+
_original_sendall = None
|
|
175
|
+
_original_read = None
|
|
176
|
+
_original_write = None
|
|
177
|
+
_patched = False
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def patch_ssl_sockets():
|
|
181
|
+
"""
|
|
182
|
+
Patch ssl.SSLSocket for tee-style HTTPS capture.
|
|
183
|
+
|
|
184
|
+
CRITICAL: Call this FIRST before patching requests/httpx/urllib3,
|
|
185
|
+
because all HTTP libraries use ssl.SSLSocket underneath.
|
|
186
|
+
|
|
187
|
+
Patches: recv, send, sendall, read, write
|
|
188
|
+
|
|
189
|
+
NOTE: By default, this function is a NO-OP (Python SSL tee is DISABLED).
|
|
190
|
+
Only when SF_ENABLE_PYTHON_SSL_TEE=1 will it actually patch SSL sockets.
|
|
191
|
+
SF_SSL_PYTHON_MODE=1 by default only disables C SSL hooks.
|
|
192
|
+
"""
|
|
193
|
+
global _original_recv, _original_send, _original_sendall, _original_read, _original_write, _patched
|
|
194
|
+
|
|
195
|
+
# Always log entry when SF_DEBUG is enabled
|
|
196
|
+
if _SF_DEBUG:
|
|
197
|
+
print(f"[ssl_socket.py] patch_ssl_sockets() CALLED - _PYTHON_SSL_TEE_ENABLED={_PYTHON_SSL_TEE_ENABLED}, _patched={_patched}, SF_ENABLE_PYTHON_SSL_TEE={os.getenv('SF_ENABLE_PYTHON_SSL_TEE', 'NOT_SET')}", log=False)
|
|
198
|
+
|
|
199
|
+
if _patched:
|
|
200
|
+
if _SF_DEBUG:
|
|
201
|
+
print("[ssl_socket.py] Already patched, skipping", log=False)
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
# Check if Python SSL tee is enabled
|
|
205
|
+
if not _PYTHON_SSL_TEE_ENABLED:
|
|
206
|
+
if _SF_DEBUG:
|
|
207
|
+
print("[ssl_socket.py] Python SSL tee DISABLED (SF_ENABLE_PYTHON_SSL_TEE not set to 'true')", log=False)
|
|
208
|
+
print("[ssl_socket.py] SF_SSL_PYTHON_MODE=1 only disables C SSL hooks (no Python capture)", log=False)
|
|
209
|
+
print("[ssl_socket.py] httpcore/requests/etc will handle their own capture instead", log=False)
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
if _SF_DEBUG:
|
|
213
|
+
print("[ssl_socket.py] Python SSL tee ENABLED - Patching ssl.SSLSocket for tee capture", log=False)
|
|
214
|
+
print("[ssl_socket.py] This should only be used for testing/debugging!", log=False)
|
|
215
|
+
print(f"[ssl_socket.py] Capture config: enabled={_CAPTURE_ENABLED}, "
|
|
216
|
+
f"req_headers={_CAPTURE_REQUEST_HEADERS}, req_body={_CAPTURE_REQUEST_BODY}, "
|
|
217
|
+
f"resp_headers={_CAPTURE_RESPONSE_HEADERS}, resp_body={_CAPTURE_RESPONSE_BODY}", log=False)
|
|
218
|
+
print(f"[ssl_socket.py] Size limits: request={_REQUEST_LIMIT_MB}MB, response={_RESPONSE_LIMIT_MB}MB", log=False)
|
|
219
|
+
|
|
220
|
+
# Initialize fast network tracking for collectNetworkRequest emission
|
|
221
|
+
init_fast_network_tracking()
|
|
222
|
+
if _SF_DEBUG:
|
|
223
|
+
print("[ssl_socket.py] Initialized fast network tracking for collectNetworkRequest", log=False)
|
|
224
|
+
|
|
225
|
+
# Save originals
|
|
226
|
+
_original_recv = ssl.SSLSocket.recv
|
|
227
|
+
_original_send = ssl.SSLSocket.send
|
|
228
|
+
_original_sendall = ssl.SSLSocket.sendall
|
|
229
|
+
_original_read = ssl.SSLSocket.read
|
|
230
|
+
_original_write = ssl.SSLSocket.write
|
|
231
|
+
|
|
232
|
+
# Patch recv/send (socket interface)
|
|
233
|
+
def fast_recv(self, bufsize, flags=0):
|
|
234
|
+
"""~15ns overhead tee wrapper"""
|
|
235
|
+
if _SF_DEBUG and not hasattr(self, '_recv_logged'):
|
|
236
|
+
self._recv_logged = True
|
|
237
|
+
print(f"[ssl_socket.py] fast_recv: FIRST CALL on sock_id={id(self)}", log=False)
|
|
238
|
+
data = _original_recv(self, bufsize, flags)
|
|
239
|
+
if data:
|
|
240
|
+
_tee_capture(self, data, 'RX')
|
|
241
|
+
return data
|
|
242
|
+
|
|
243
|
+
def fast_send(self, data, flags=0):
|
|
244
|
+
"""~15ns overhead tee wrapper"""
|
|
245
|
+
if _SF_DEBUG and not hasattr(self, '_send_logged'):
|
|
246
|
+
self._send_logged = True
|
|
247
|
+
print(f"[ssl_socket.py] fast_send: FIRST CALL on sock_id={id(self)}, sending {len(data)} bytes", log=False)
|
|
248
|
+
result = _original_send(self, data, flags)
|
|
249
|
+
if result > 0:
|
|
250
|
+
_tee_capture(self, data[:result], 'TX')
|
|
251
|
+
return result
|
|
252
|
+
|
|
253
|
+
def fast_sendall(self, data):
|
|
254
|
+
"""~15ns overhead tee wrapper for sendall()"""
|
|
255
|
+
# sendall() doesn't return number of bytes sent, it sends all or raises exception
|
|
256
|
+
if _SF_DEBUG and not hasattr(self, '_sendall_logged'):
|
|
257
|
+
self._sendall_logged = True
|
|
258
|
+
print(f"[ssl_socket.py] fast_sendall: FIRST CALL on sock_id={id(self)}, sending {len(data)} bytes", log=False)
|
|
259
|
+
_original_sendall(self, data)
|
|
260
|
+
# If we get here, all data was sent
|
|
261
|
+
_tee_capture(self, data, 'TX')
|
|
262
|
+
return None # sendall returns None on success
|
|
263
|
+
|
|
264
|
+
# Patch read/write (file interface)
|
|
265
|
+
def fast_read(self, len=1024, buffer=None):
|
|
266
|
+
"""~15ns overhead tee wrapper"""
|
|
267
|
+
if _SF_DEBUG and not hasattr(self, '_read_logged'):
|
|
268
|
+
self._read_logged = True
|
|
269
|
+
print(f"[ssl_socket.py] fast_read: FIRST CALL on sock_id={id(self)}", log=False)
|
|
270
|
+
data = _original_read(self, len, buffer)
|
|
271
|
+
if buffer is not None:
|
|
272
|
+
# readinto mode: data is int (bytes read), actual data is in buffer
|
|
273
|
+
if data and isinstance(data, int) and data > 0:
|
|
274
|
+
# Extract bytes from buffer for capture
|
|
275
|
+
captured_data = bytes(buffer[:data])
|
|
276
|
+
_tee_capture(self, captured_data, 'RX')
|
|
277
|
+
elif data:
|
|
278
|
+
# Normal mode: data is bytes
|
|
279
|
+
_tee_capture(self, data, 'RX')
|
|
280
|
+
return data
|
|
281
|
+
|
|
282
|
+
def fast_write(self, data):
|
|
283
|
+
"""~15ns overhead tee wrapper"""
|
|
284
|
+
if _SF_DEBUG and not hasattr(self, '_write_logged'):
|
|
285
|
+
self._write_logged = True
|
|
286
|
+
print(f"[ssl_socket.py] fast_write: FIRST CALL on sock_id={id(self)}, writing {len(data)} bytes", log=False)
|
|
287
|
+
result = _original_write(self, data)
|
|
288
|
+
if result > 0:
|
|
289
|
+
_tee_capture(self, data[:result], 'TX')
|
|
290
|
+
return result
|
|
291
|
+
|
|
292
|
+
# Apply patches
|
|
293
|
+
ssl.SSLSocket.recv = fast_recv
|
|
294
|
+
ssl.SSLSocket.send = fast_send
|
|
295
|
+
ssl.SSLSocket.sendall = fast_sendall
|
|
296
|
+
ssl.SSLSocket.read = fast_read
|
|
297
|
+
ssl.SSLSocket.write = fast_write
|
|
298
|
+
|
|
299
|
+
# Cache peername on connect to avoid repeated syscalls on hot path
|
|
300
|
+
_patch_ssl_connect()
|
|
301
|
+
|
|
302
|
+
# Start background processor
|
|
303
|
+
_start_background_processor()
|
|
304
|
+
|
|
305
|
+
_patched = True
|
|
306
|
+
|
|
307
|
+
if _SF_DEBUG:
|
|
308
|
+
print("[ssl_socket.py] ✓ SSL patching complete - all HTTPS traffic will be captured automatically", log=False)
|
|
309
|
+
print("[ssl_socket.py] Patched methods: recv, send, sendall, read, write", log=False)
|
|
310
|
+
print("[ssl_socket.py] requests, httpx, urllib3, aiohttp all use ssl.SSLSocket underneath", log=False)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _patch_ssl_connect():
|
|
314
|
+
"""Cache peername after SSL handshake to avoid syscalls on hot path"""
|
|
315
|
+
original_do_handshake = ssl.SSLSocket.do_handshake
|
|
316
|
+
|
|
317
|
+
def cached_do_handshake(self):
|
|
318
|
+
result = original_do_handshake(self)
|
|
319
|
+
try:
|
|
320
|
+
self._peername_cache = self.getpeername()
|
|
321
|
+
except Exception:
|
|
322
|
+
pass
|
|
323
|
+
return result
|
|
324
|
+
|
|
325
|
+
ssl.SSLSocket.do_handshake = cached_do_handshake
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
# ============================================================================
|
|
329
|
+
# BACKGROUND PROCESSING (Zero hot path impact)
|
|
330
|
+
# ============================================================================
|
|
331
|
+
|
|
332
|
+
_background_thread = None
|
|
333
|
+
_background_event = threading.Event()
|
|
334
|
+
_background_running = False
|
|
335
|
+
_sock_aggregators = {} # sock_id -> HTTPAggregator
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
class HTTPAggregator:
|
|
339
|
+
"""Aggregates captured bytes into complete HTTP transactions"""
|
|
340
|
+
|
|
341
|
+
def __init__(self, sock_id):
|
|
342
|
+
self.sock_id = sock_id
|
|
343
|
+
self.tx_buffer = bytearray()
|
|
344
|
+
self.rx_buffer = bytearray()
|
|
345
|
+
self.current_transaction = None
|
|
346
|
+
self.last_activity = time.time()
|
|
347
|
+
self.peername = None
|
|
348
|
+
self._first_feed_logged = False # Debug flag
|
|
349
|
+
|
|
350
|
+
def feed(self, data, direction, peername=None):
|
|
351
|
+
"""Feed captured data, parse when complete"""
|
|
352
|
+
# Early exit if capture is disabled globally
|
|
353
|
+
if not _CAPTURE_ENABLED:
|
|
354
|
+
return
|
|
355
|
+
|
|
356
|
+
# Debug: Log first feed for this socket
|
|
357
|
+
if _SF_DEBUG and not self._first_feed_logged:
|
|
358
|
+
self._first_feed_logged = True
|
|
359
|
+
print(f"[ssl_socket.py] HTTPAggregator.feed: FIRST FEED sock_id={self.sock_id}, direction={direction}, size={len(data)} bytes", log=False)
|
|
360
|
+
|
|
361
|
+
if peername:
|
|
362
|
+
self.peername = peername
|
|
363
|
+
|
|
364
|
+
if direction == 'TX':
|
|
365
|
+
# Only buffer request data if we're capturing headers or body
|
|
366
|
+
if _CAPTURE_REQUEST_HEADERS or _CAPTURE_REQUEST_BODY:
|
|
367
|
+
self.tx_buffer.extend(data)
|
|
368
|
+
if _SF_DEBUG:
|
|
369
|
+
print(f"[ssl_socket.py] HTTPAggregator.feed: TX buffer now {len(self.tx_buffer)} bytes, calling _try_parse_request()", log=False)
|
|
370
|
+
self._try_parse_request()
|
|
371
|
+
else: # RX
|
|
372
|
+
# Only buffer response data if we're capturing headers or body
|
|
373
|
+
if _CAPTURE_RESPONSE_HEADERS or _CAPTURE_RESPONSE_BODY:
|
|
374
|
+
self.rx_buffer.extend(data)
|
|
375
|
+
if _SF_DEBUG:
|
|
376
|
+
print(f"[ssl_socket.py] HTTPAggregator.feed: RX buffer now {len(self.rx_buffer)} bytes, calling _try_parse_response()", log=False)
|
|
377
|
+
self._try_parse_response()
|
|
378
|
+
|
|
379
|
+
self.last_activity = time.time()
|
|
380
|
+
|
|
381
|
+
def _try_parse_request(self):
|
|
382
|
+
"""Try to parse complete HTTP request from buffer"""
|
|
383
|
+
# Look for \r\n\r\n (end of headers)
|
|
384
|
+
headers_end = self.tx_buffer.find(b'\r\n\r\n')
|
|
385
|
+
if headers_end == -1:
|
|
386
|
+
if _SF_DEBUG and len(self.tx_buffer) > 0:
|
|
387
|
+
print(f"[ssl_socket.py] _try_parse_request: Incomplete headers (buffer={len(self.tx_buffer)} bytes, waiting for \\r\\n\\r\\n)", log=False)
|
|
388
|
+
return # Incomplete headers
|
|
389
|
+
|
|
390
|
+
try:
|
|
391
|
+
headers = self.tx_buffer[:headers_end].decode('latin-1', errors='ignore')
|
|
392
|
+
except Exception:
|
|
393
|
+
return
|
|
394
|
+
|
|
395
|
+
if _SF_DEBUG:
|
|
396
|
+
preview = headers if len(headers) < 1000 else headers[:1000] + "...<truncated>"
|
|
397
|
+
print(f"[ssl_socket.py] RAW REQUEST HEADERS:\n{preview}", log=False)
|
|
398
|
+
|
|
399
|
+
lines = headers.split('\r\n')
|
|
400
|
+
|
|
401
|
+
if not lines:
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
# Parse request line
|
|
405
|
+
request_line = lines[0].split(' ', 2)
|
|
406
|
+
if len(request_line) < 3:
|
|
407
|
+
return
|
|
408
|
+
|
|
409
|
+
method, target, version = request_line
|
|
410
|
+
|
|
411
|
+
# Parse headers
|
|
412
|
+
host = None
|
|
413
|
+
content_length = 0
|
|
414
|
+
for line in lines[1:]:
|
|
415
|
+
if ':' in line:
|
|
416
|
+
key, value = line.split(':', 1)
|
|
417
|
+
key = key.strip().lower()
|
|
418
|
+
value = value.strip()
|
|
419
|
+
if key == 'host':
|
|
420
|
+
host = value
|
|
421
|
+
elif key == 'content-length':
|
|
422
|
+
try:
|
|
423
|
+
content_length = int(value)
|
|
424
|
+
except ValueError:
|
|
425
|
+
pass
|
|
426
|
+
|
|
427
|
+
# Use peername as fallback for host
|
|
428
|
+
if not host and self.peername:
|
|
429
|
+
host = f"{self.peername[0]}:{self.peername[1]}"
|
|
430
|
+
|
|
431
|
+
# Check if body complete
|
|
432
|
+
body_start = headers_end + 4
|
|
433
|
+
body_available = len(self.tx_buffer) - body_start
|
|
434
|
+
|
|
435
|
+
# Capture up to configured limit
|
|
436
|
+
body_capture_limit = MAX_REQUEST_BODY_CAPTURE if _CAPTURE_REQUEST_BODY else 0
|
|
437
|
+
expected_body_size = min(content_length, body_capture_limit) if _CAPTURE_REQUEST_BODY else 0
|
|
438
|
+
|
|
439
|
+
if body_available >= expected_body_size:
|
|
440
|
+
# Request complete!
|
|
441
|
+
body = self.tx_buffer[body_start:body_start + expected_body_size] if _CAPTURE_REQUEST_BODY else b''
|
|
442
|
+
|
|
443
|
+
self.current_transaction = {
|
|
444
|
+
'method': method,
|
|
445
|
+
'target': target,
|
|
446
|
+
'host': host or 'unknown',
|
|
447
|
+
'req_headers': headers if _CAPTURE_REQUEST_HEADERS else '',
|
|
448
|
+
'req_body': bytes(body),
|
|
449
|
+
't_start': time.perf_counter_ns()
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if _SF_DEBUG:
|
|
453
|
+
print(f"[ssl_socket.py] _try_parse_request: REQUEST COMPLETE - {method} {host}{target}, waiting for response", log=False)
|
|
454
|
+
print(f"[ssl_socket.py] _try_parse_request: Captured req_headers_len={len(headers) if _CAPTURE_REQUEST_HEADERS else 0}, req_body_len={len(body)}", log=False)
|
|
455
|
+
if _CAPTURE_REQUEST_HEADERS and len(headers) > 0:
|
|
456
|
+
header_lines = headers.split('\r\n')[:5] # First 5 lines
|
|
457
|
+
print(f"[ssl_socket.py] _try_parse_request: First headers lines: {header_lines}", log=False)
|
|
458
|
+
|
|
459
|
+
# Clear buffer
|
|
460
|
+
self.tx_buffer.clear()
|
|
461
|
+
|
|
462
|
+
def _try_parse_response(self):
|
|
463
|
+
"""Try to parse complete HTTP response from buffer"""
|
|
464
|
+
if not self.current_transaction:
|
|
465
|
+
if _SF_DEBUG and len(self.rx_buffer) > 0:
|
|
466
|
+
print(f"[ssl_socket.py] _try_parse_response: No current transaction (received response without request?), buffer={len(self.rx_buffer)} bytes", log=False)
|
|
467
|
+
return # No request to match
|
|
468
|
+
|
|
469
|
+
# Look for \r\n\r\n
|
|
470
|
+
headers_end = self.rx_buffer.find(b'\r\n\r\n')
|
|
471
|
+
if headers_end == -1:
|
|
472
|
+
if _SF_DEBUG and len(self.rx_buffer) > 0:
|
|
473
|
+
print(f"[ssl_socket.py] _try_parse_response: Incomplete headers (buffer={len(self.rx_buffer)} bytes, waiting for \\r\\n\\r\\n)", log=False)
|
|
474
|
+
return
|
|
475
|
+
|
|
476
|
+
try:
|
|
477
|
+
headers = self.rx_buffer[:headers_end].decode('latin-1', errors='ignore')
|
|
478
|
+
except Exception:
|
|
479
|
+
return
|
|
480
|
+
|
|
481
|
+
lines = headers.split('\r\n')
|
|
482
|
+
|
|
483
|
+
if not lines:
|
|
484
|
+
return
|
|
485
|
+
|
|
486
|
+
# Parse status line
|
|
487
|
+
status_line = lines[0].split(' ', 2)
|
|
488
|
+
status = 0
|
|
489
|
+
if len(status_line) >= 2:
|
|
490
|
+
try:
|
|
491
|
+
status = int(status_line[1])
|
|
492
|
+
except ValueError:
|
|
493
|
+
pass
|
|
494
|
+
|
|
495
|
+
# Parse content-length
|
|
496
|
+
content_length = 0
|
|
497
|
+
for line in lines[1:]:
|
|
498
|
+
if ':' in line:
|
|
499
|
+
key, value = line.split(':', 1)
|
|
500
|
+
if key.strip().lower() == 'content-length':
|
|
501
|
+
try:
|
|
502
|
+
content_length = int(value.strip())
|
|
503
|
+
except ValueError:
|
|
504
|
+
pass
|
|
505
|
+
|
|
506
|
+
# Check if body complete (capture up to configured limit)
|
|
507
|
+
body_start = headers_end + 4
|
|
508
|
+
body_available = len(self.rx_buffer) - body_start
|
|
509
|
+
|
|
510
|
+
# Capture up to configured limit
|
|
511
|
+
body_capture_limit = MAX_RESPONSE_BODY_CAPTURE if _CAPTURE_RESPONSE_BODY else 0
|
|
512
|
+
expected_body_size = min(content_length, body_capture_limit) if _CAPTURE_RESPONSE_BODY else 0
|
|
513
|
+
|
|
514
|
+
if body_available >= expected_body_size:
|
|
515
|
+
# Response complete!
|
|
516
|
+
body = self.rx_buffer[body_start:body_start + expected_body_size] if _CAPTURE_RESPONSE_BODY else b''
|
|
517
|
+
|
|
518
|
+
self.current_transaction.update({
|
|
519
|
+
'status': status,
|
|
520
|
+
'resp_headers': headers if _CAPTURE_RESPONSE_HEADERS else '',
|
|
521
|
+
'resp_body': bytes(body),
|
|
522
|
+
't_end': time.perf_counter_ns()
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
if _SF_DEBUG:
|
|
526
|
+
method = self.current_transaction.get('method', 'UNKNOWN')
|
|
527
|
+
host = self.current_transaction.get('host', 'unknown')
|
|
528
|
+
target = self.current_transaction.get('target', '/')
|
|
529
|
+
print(f"[ssl_socket.py] _try_parse_response: RESPONSE COMPLETE - {method} {host}{target} -> status={status}, pushing to ring", log=False)
|
|
530
|
+
print(f"[ssl_socket.py] _try_parse_response: Captured resp_headers_len={len(headers) if _CAPTURE_RESPONSE_HEADERS else 0}, resp_body_len={len(body)}", log=False)
|
|
531
|
+
if _CAPTURE_RESPONSE_HEADERS and len(headers) > 0:
|
|
532
|
+
header_lines = headers.split('\r\n')[:5] # First 5 lines
|
|
533
|
+
print(f"[ssl_socket.py] _try_parse_response: First headers lines: {header_lines}", log=False)
|
|
534
|
+
if len(body) > 0:
|
|
535
|
+
print(f"[ssl_socket.py] _try_parse_response: Body preview: {body[:100]}", log=False)
|
|
536
|
+
|
|
537
|
+
# Push to C ring
|
|
538
|
+
_push_transaction_to_c_ring(self.current_transaction)
|
|
539
|
+
|
|
540
|
+
# Clear state
|
|
541
|
+
self.current_transaction = None
|
|
542
|
+
self.rx_buffer.clear()
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def _background_processor():
|
|
546
|
+
"""Background thread that aggregates and pushes to C ring"""
|
|
547
|
+
global _background_running
|
|
548
|
+
_background_running = True
|
|
549
|
+
|
|
550
|
+
if _SF_DEBUG:
|
|
551
|
+
print("[ssl_socket.py] Background processor started", log=False)
|
|
552
|
+
|
|
553
|
+
items_processed = 0 # Debug counter
|
|
554
|
+
|
|
555
|
+
while _background_running:
|
|
556
|
+
_background_event.wait(timeout=0.01) # 10ms or on signal
|
|
557
|
+
_background_event.clear()
|
|
558
|
+
|
|
559
|
+
# Drain all capture queues
|
|
560
|
+
batch_count = 0
|
|
561
|
+
for tid, queue in list(_capture_queues.items()):
|
|
562
|
+
while queue:
|
|
563
|
+
try:
|
|
564
|
+
item = queue.popleft()
|
|
565
|
+
sock_id = item['sock_id']
|
|
566
|
+
batch_count += 1
|
|
567
|
+
|
|
568
|
+
# Get or create aggregator for this socket
|
|
569
|
+
if sock_id not in _sock_aggregators:
|
|
570
|
+
_sock_aggregators[sock_id] = HTTPAggregator(sock_id)
|
|
571
|
+
if _SF_DEBUG:
|
|
572
|
+
print(f"[ssl_socket.py] _background_processor: Created new aggregator for sock_id={sock_id}", log=False)
|
|
573
|
+
|
|
574
|
+
agg = _sock_aggregators[sock_id]
|
|
575
|
+
agg.feed(item['data'], item['direction'], item.get('peername'))
|
|
576
|
+
|
|
577
|
+
except Exception as e:
|
|
578
|
+
if _SF_DEBUG:
|
|
579
|
+
print(f"[ssl_socket.py] Background processing error: {e}", log=False)
|
|
580
|
+
|
|
581
|
+
# Debug: Log processing activity
|
|
582
|
+
if _SF_DEBUG and batch_count > 0:
|
|
583
|
+
items_processed += batch_count
|
|
584
|
+
print(f"[ssl_socket.py] _background_processor: Processed {batch_count} items (total: {items_processed})", log=False)
|
|
585
|
+
|
|
586
|
+
# Cleanup stale aggregators (>30s idle)
|
|
587
|
+
now = time.time()
|
|
588
|
+
stale = [sid for sid, agg in _sock_aggregators.items()
|
|
589
|
+
if now - agg.last_activity > 30]
|
|
590
|
+
for sid in stale:
|
|
591
|
+
del _sock_aggregators[sid]
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def _start_background_processor():
|
|
595
|
+
"""Start background processor thread"""
|
|
596
|
+
global _background_thread
|
|
597
|
+
if _background_thread is None:
|
|
598
|
+
_background_thread = threading.Thread(
|
|
599
|
+
target=_background_processor,
|
|
600
|
+
daemon=True,
|
|
601
|
+
name='ssl_capture_processor'
|
|
602
|
+
)
|
|
603
|
+
_background_thread.start()
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
# ============================================================================
|
|
607
|
+
# C RING BRIDGE (Called from background thread only)
|
|
608
|
+
# ============================================================================
|
|
609
|
+
|
|
610
|
+
_c_lib = None
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _init_c_bridge():
|
|
614
|
+
"""Initialize ctypes bridge to C ring buffer"""
|
|
615
|
+
global _c_lib
|
|
616
|
+
|
|
617
|
+
if not _C_RING_AVAILABLE:
|
|
618
|
+
if _SF_DEBUG:
|
|
619
|
+
print("[ssl_socket.py] C ring not available (SF_SSL_PYTHON_MODE not set)", log=False)
|
|
620
|
+
return False
|
|
621
|
+
|
|
622
|
+
try:
|
|
623
|
+
# Try to load the library
|
|
624
|
+
import sf_veritas
|
|
625
|
+
lib_path = os.path.join(os.path.dirname(sf_veritas.__file__), 'libsfnettee.so')
|
|
626
|
+
|
|
627
|
+
if not os.path.exists(lib_path):
|
|
628
|
+
if _SF_DEBUG:
|
|
629
|
+
print(f"[ssl_socket.py] libsfnettee.so not found at {lib_path}", log=False)
|
|
630
|
+
return False
|
|
631
|
+
|
|
632
|
+
_c_lib = ctypes.CDLL(lib_path)
|
|
633
|
+
|
|
634
|
+
# Define function signature
|
|
635
|
+
_c_lib.sf_ring_push_from_python.argtypes = [
|
|
636
|
+
ctypes.c_char_p, # req_method
|
|
637
|
+
ctypes.c_char_p, # req_target
|
|
638
|
+
ctypes.c_char_p, # req_host
|
|
639
|
+
ctypes.c_char_p, # req_headers
|
|
640
|
+
ctypes.c_char_p, # req_body
|
|
641
|
+
ctypes.c_size_t, # req_body_len
|
|
642
|
+
ctypes.c_char_p, # resp_headers
|
|
643
|
+
ctypes.c_char_p, # resp_body
|
|
644
|
+
ctypes.c_size_t, # resp_body_len
|
|
645
|
+
ctypes.c_int, # resp_status
|
|
646
|
+
ctypes.c_uint64, # t_start_ns
|
|
647
|
+
ctypes.c_uint64, # t_end_ns
|
|
648
|
+
ctypes.c_char_p, # parent_trace_id
|
|
649
|
+
ctypes.c_int, # is_ssl
|
|
650
|
+
]
|
|
651
|
+
_c_lib.sf_ring_push_from_python.restype = ctypes.c_int
|
|
652
|
+
|
|
653
|
+
if _SF_DEBUG:
|
|
654
|
+
print("[ssl_socket.py] ✓ C ring bridge initialized successfully", log=False)
|
|
655
|
+
|
|
656
|
+
return True
|
|
657
|
+
|
|
658
|
+
except Exception as e:
|
|
659
|
+
if _SF_DEBUG:
|
|
660
|
+
print(f"[ssl_socket.py] Failed to init C bridge: {e}", log=False)
|
|
661
|
+
return False
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def _debug_log_sf_header_anomalies(raw_headers: str) -> None:
|
|
665
|
+
"""Log duplicate or mismatched Sailfish headers when SF_DEBUG enabled."""
|
|
666
|
+
if not (_SF_DEBUG and raw_headers):
|
|
667
|
+
return
|
|
668
|
+
|
|
669
|
+
sf3_values = []
|
|
670
|
+
sf4_values = []
|
|
671
|
+
for line in raw_headers.split('\r\n'):
|
|
672
|
+
if not line or ':' not in line:
|
|
673
|
+
continue
|
|
674
|
+
key, value = line.split(':', 1)
|
|
675
|
+
key_strip = key.strip()
|
|
676
|
+
value_strip = value.strip()
|
|
677
|
+
key_lower = key_strip.lower()
|
|
678
|
+
if key_lower == 'x-sf3-rid':
|
|
679
|
+
sf3_values.append((key_strip, value_strip))
|
|
680
|
+
elif key_lower == 'x-sf4-prid':
|
|
681
|
+
sf4_values.append((key_strip, value_strip))
|
|
682
|
+
|
|
683
|
+
if len(sf3_values) > 1:
|
|
684
|
+
print(f"[ssl_socket.py] ⚠️ Multiple X-Sf3-Rid headers detected: {sf3_values}", log=False)
|
|
685
|
+
if len(sf4_values) > 1:
|
|
686
|
+
print(f"[ssl_socket.py] ⚠️ Multiple X-Sf4-Prid headers detected: {sf4_values}", log=False)
|
|
687
|
+
if sf3_values and sf4_values and sf3_values[0][1] == sf4_values[0][1]:
|
|
688
|
+
print(
|
|
689
|
+
"[ssl_socket.py] ⚠️ X-Sf3-Rid matches X-Sf4-Prid "
|
|
690
|
+
f"(possible parent propagation) sf3={sf3_values} sf4={sf4_values}",
|
|
691
|
+
log=False,
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def _push_transaction_to_c_ring(transaction: Dict[str, Any]):
|
|
696
|
+
"""
|
|
697
|
+
Emit collectNetworkRequest mutation via record_network_request().
|
|
698
|
+
Respects SF_NETWORKREQUEST_CAPTURE_* environment variables.
|
|
699
|
+
|
|
700
|
+
NOTE: This now uses record_network_request() instead of sf_ring_push_from_python()
|
|
701
|
+
to emit the standard collectNetworkRequest mutation like other library patches.
|
|
702
|
+
"""
|
|
703
|
+
# Early exit if capture is disabled
|
|
704
|
+
if not _CAPTURE_ENABLED:
|
|
705
|
+
if _SF_DEBUG:
|
|
706
|
+
print(f"[ssl_socket.py] _push_transaction_to_c_ring: capture disabled, skipping", log=False)
|
|
707
|
+
return
|
|
708
|
+
|
|
709
|
+
if _SF_DEBUG:
|
|
710
|
+
method = transaction.get('method', 'UNKNOWN')
|
|
711
|
+
host = transaction.get('host', 'unknown')
|
|
712
|
+
target = transaction.get('target', '/')
|
|
713
|
+
status = transaction.get('status', 0)
|
|
714
|
+
print(f"[ssl_socket.py] _push_transaction_to_c_ring: DATA RECEIVED - {method} {host}{target} -> status={status}", log=False)
|
|
715
|
+
|
|
716
|
+
try:
|
|
717
|
+
# Extract trace_id from X-Sf3-Rid header (injected by outbound header manager)
|
|
718
|
+
trace_id = ""
|
|
719
|
+
req_headers_str = transaction.get('req_headers', '')
|
|
720
|
+
if req_headers_str:
|
|
721
|
+
_debug_log_sf_header_anomalies(req_headers_str)
|
|
722
|
+
# Parse X-Sf3-Rid header from raw HTTP headers
|
|
723
|
+
for line in req_headers_str.split('\r\n'):
|
|
724
|
+
if ':' in line:
|
|
725
|
+
key, value = line.split(':', 1)
|
|
726
|
+
if key.strip().lower() == 'x-sf3-rid':
|
|
727
|
+
trace_id = value.strip()
|
|
728
|
+
break
|
|
729
|
+
|
|
730
|
+
# Build full URL (SSL socket only captures HTTPS)
|
|
731
|
+
host = transaction.get('host', 'unknown')
|
|
732
|
+
target = transaction.get('target', '/')
|
|
733
|
+
url = f"https://{host}{target}"
|
|
734
|
+
|
|
735
|
+
# Convert headers from raw HTTP format to JSON dict
|
|
736
|
+
import json
|
|
737
|
+
try:
|
|
738
|
+
import orjson
|
|
739
|
+
HAS_ORJSON = True
|
|
740
|
+
except ImportError:
|
|
741
|
+
HAS_ORJSON = False
|
|
742
|
+
|
|
743
|
+
def parse_http_headers(headers_str: str) -> bytes:
|
|
744
|
+
"""Parse raw HTTP headers into JSON dict."""
|
|
745
|
+
if not headers_str:
|
|
746
|
+
return b"{}"
|
|
747
|
+
headers_dict = {}
|
|
748
|
+
for line in headers_str.split('\r\n')[1:]: # Skip request/status line
|
|
749
|
+
if ':' in line:
|
|
750
|
+
key, value = line.split(':', 1)
|
|
751
|
+
headers_dict[key.strip()] = value.strip()
|
|
752
|
+
if HAS_ORJSON:
|
|
753
|
+
return orjson.dumps(headers_dict)
|
|
754
|
+
else:
|
|
755
|
+
return json.dumps(headers_dict).encode('utf-8')
|
|
756
|
+
|
|
757
|
+
# Prepare data based on capture flags
|
|
758
|
+
req_headers_json = parse_http_headers(req_headers_str) if _CAPTURE_REQUEST_HEADERS else b'{}'
|
|
759
|
+
req_body = transaction.get('req_body', b'') if _CAPTURE_REQUEST_BODY else b''
|
|
760
|
+
|
|
761
|
+
resp_headers_str = transaction.get('resp_headers', '')
|
|
762
|
+
resp_headers_json = parse_http_headers(resp_headers_str) if _CAPTURE_RESPONSE_HEADERS else b'{}'
|
|
763
|
+
resp_body = transaction.get('resp_body', b'') if _CAPTURE_RESPONSE_BODY else b''
|
|
764
|
+
|
|
765
|
+
# Enforce size limits (already enforced during parsing, but double-check)
|
|
766
|
+
if len(req_body) > MAX_REQUEST_BODY_CAPTURE:
|
|
767
|
+
req_body = req_body[:MAX_REQUEST_BODY_CAPTURE]
|
|
768
|
+
if len(resp_body) > MAX_RESPONSE_BODY_CAPTURE:
|
|
769
|
+
resp_body = resp_body[:MAX_RESPONSE_BODY_CAPTURE]
|
|
770
|
+
|
|
771
|
+
# Convert timestamps from nanoseconds to milliseconds
|
|
772
|
+
timestamp_start = transaction['t_start'] // 1_000_000 # ns to ms
|
|
773
|
+
timestamp_end = transaction.get('t_end', transaction['t_start']) // 1_000_000 # ns to ms
|
|
774
|
+
|
|
775
|
+
# Get status and determine success
|
|
776
|
+
status = transaction.get('status', 0)
|
|
777
|
+
success = status > 0 and status < 400
|
|
778
|
+
|
|
779
|
+
if _SF_DEBUG:
|
|
780
|
+
print(f"[ssl_socket.py] PREPARING TO SEND collectNetworkRequest:", log=False)
|
|
781
|
+
print(f"[ssl_socket.py] url={url} (type={type(url).__name__})", log=False)
|
|
782
|
+
print(f"[ssl_socket.py] method={transaction['method']} (type={type(transaction['method']).__name__})", log=False)
|
|
783
|
+
print(f"[ssl_socket.py] status_code={status}", log=False)
|
|
784
|
+
print(f"[ssl_socket.py] success={success}", log=False)
|
|
785
|
+
print(f"[ssl_socket.py] trace_id={trace_id}", log=False)
|
|
786
|
+
print(f"[ssl_socket.py] request_headers_size={len(req_headers_json)} bytes", log=False)
|
|
787
|
+
print(f"[ssl_socket.py] request_body_size={len(req_body)} bytes", log=False)
|
|
788
|
+
print(f"[ssl_socket.py] response_headers_size={len(resp_headers_json)} bytes", log=False)
|
|
789
|
+
print(f"[ssl_socket.py] response_body_size={len(resp_body)} bytes", log=False)
|
|
790
|
+
print(f"[ssl_socket.py] timestamp_start={timestamp_start}ms", log=False)
|
|
791
|
+
print(f"[ssl_socket.py] timestamp_end={timestamp_end}ms", log=False)
|
|
792
|
+
# Show first 200 chars of headers/body for debugging
|
|
793
|
+
if len(req_headers_json) > 2:
|
|
794
|
+
print(f"[ssl_socket.py] req_headers_preview={req_headers_json[:200]}", log=False)
|
|
795
|
+
if len(resp_headers_json) > 2:
|
|
796
|
+
print(f"[ssl_socket.py] resp_headers_preview={resp_headers_json[:200]}", log=False)
|
|
797
|
+
if len(req_body) > 0:
|
|
798
|
+
print(f"[ssl_socket.py] req_body_preview={req_body[:100]}", log=False)
|
|
799
|
+
if len(resp_body) > 0:
|
|
800
|
+
print(f"[ssl_socket.py] resp_body_preview={resp_body[:100]}", log=False)
|
|
801
|
+
|
|
802
|
+
# Emit collectNetworkRequest mutation
|
|
803
|
+
record_network_request(
|
|
804
|
+
trace_id=trace_id,
|
|
805
|
+
url=url,
|
|
806
|
+
method=transaction['method'],
|
|
807
|
+
status_code=status,
|
|
808
|
+
success=success,
|
|
809
|
+
timestamp_start=timestamp_start,
|
|
810
|
+
timestamp_end=timestamp_end,
|
|
811
|
+
request_data=req_body,
|
|
812
|
+
response_data=resp_body,
|
|
813
|
+
request_headers=req_headers_json,
|
|
814
|
+
response_headers=resp_headers_json,
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
if _SF_DEBUG:
|
|
818
|
+
print(f"[ssl_socket.py] ✓ SUCCESSFULLY SENT collectNetworkRequest for {transaction['method']} {url} (status={status})", log=False)
|
|
819
|
+
|
|
820
|
+
except Exception as e:
|
|
821
|
+
if _SF_DEBUG:
|
|
822
|
+
print(f"[ssl_socket.py] Failed to emit collectNetworkRequest: {e}", log=False)
|