h4-debug 0.1.4__tar.gz → 0.2.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.
- {h4_debug-0.1.4 → h4_debug-0.2.0}/PKG-INFO +2 -1
- h4_debug-0.2.0/h4_debug/__init__.py +1 -0
- h4_debug-0.2.0/h4_debug/cli.py +71 -0
- h4_debug-0.2.0/h4_debug/handlers.py +252 -0
- {h4_debug-0.1.4 → h4_debug-0.2.0}/h4_debug/interceptor.py +5 -1
- h4_debug-0.2.0/h4_debug/node_interceptor.js +126 -0
- {h4_debug-0.1.4 → h4_debug-0.2.0}/h4_debug/static/dashboard.js +41 -0
- h4_debug-0.2.0/h4_debug/win_debugger.py +227 -0
- {h4_debug-0.1.4 → h4_debug-0.2.0}/h4_debug.egg-info/PKG-INFO +2 -1
- {h4_debug-0.1.4 → h4_debug-0.2.0}/h4_debug.egg-info/SOURCES.txt +3 -0
- {h4_debug-0.1.4 → h4_debug-0.2.0}/h4_debug.egg-info/requires.txt +1 -0
- {h4_debug-0.1.4 → h4_debug-0.2.0}/pyproject.toml +3 -2
- h4_debug-0.1.4/h4_debug/__init__.py +0 -1
- h4_debug-0.1.4/h4_debug/cli.py +0 -103
- {h4_debug-0.1.4 → h4_debug-0.2.0}/README.md +0 -0
- {h4_debug-0.1.4 → h4_debug-0.2.0}/h4_debug/server.py +0 -0
- {h4_debug-0.1.4 → h4_debug-0.2.0}/h4_debug/static/dashboard.css +0 -0
- {h4_debug-0.1.4 → h4_debug-0.2.0}/h4_debug/static/dashboard.html +0 -0
- {h4_debug-0.1.4 → h4_debug-0.2.0}/h4_debug.egg-info/dependency_links.txt +0 -0
- {h4_debug-0.1.4 → h4_debug-0.2.0}/h4_debug.egg-info/entry_points.txt +0 -0
- {h4_debug-0.1.4 → h4_debug-0.2.0}/h4_debug.egg-info/top_level.txt +0 -0
- {h4_debug-0.1.4 → h4_debug-0.2.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: h4-debug
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Advanced application proxy debugger and F12-style web console
|
|
5
5
|
Author-email: h4 <h4@example.com>
|
|
6
6
|
Requires-Python: >=3.8
|
|
@@ -8,6 +8,7 @@ Description-Content-Type: text/markdown
|
|
|
8
8
|
Requires-Dist: fastapi>=0.100
|
|
9
9
|
Requires-Dist: uvicorn[standard]>=0.20
|
|
10
10
|
Requires-Dist: websockets>=11.0
|
|
11
|
+
Requires-Dist: Pillow>=10.0.0
|
|
11
12
|
|
|
12
13
|
# h4-debug
|
|
13
14
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.5"
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
import webbrowser
|
|
7
|
+
|
|
8
|
+
from . import server
|
|
9
|
+
from . import handlers
|
|
10
|
+
|
|
11
|
+
def main():
|
|
12
|
+
parser = argparse.ArgumentParser(description="h4-debug: Advanced application proxy debugger", usage="h4-debug [--mode MODE] command ...")
|
|
13
|
+
parser.add_argument("--mode", choices=["Normal", "Trace", "Full"], default="Normal", help="Debugging mode")
|
|
14
|
+
parser.add_argument("--port", type=int, default=8008, help="Port for the web dashboard")
|
|
15
|
+
parser.add_argument("command", nargs=argparse.REMAINDER, help="Command to run")
|
|
16
|
+
|
|
17
|
+
args = parser.parse_args()
|
|
18
|
+
|
|
19
|
+
if not args.command:
|
|
20
|
+
parser.print_help()
|
|
21
|
+
sys.exit(1)
|
|
22
|
+
|
|
23
|
+
command = args.command
|
|
24
|
+
if command[0] == "--":
|
|
25
|
+
command = command[1:]
|
|
26
|
+
|
|
27
|
+
target = command[0]
|
|
28
|
+
|
|
29
|
+
# 1. Start the web server in a background thread
|
|
30
|
+
print(f"[h4-debug] Starting dashboard on http://localhost:{args.port} ...")
|
|
31
|
+
server_thread = threading.Thread(target=server.run_server, args=(args.port,), daemon=True)
|
|
32
|
+
server_thread.start()
|
|
33
|
+
|
|
34
|
+
time.sleep(1.0)
|
|
35
|
+
webbrowser.open(f"http://localhost:{args.port}")
|
|
36
|
+
|
|
37
|
+
# 2. Setup interception environment
|
|
38
|
+
env = os.environ.copy()
|
|
39
|
+
|
|
40
|
+
# 3. Dispatch to handler based on file extension
|
|
41
|
+
ext = ""
|
|
42
|
+
if os.path.isfile(target):
|
|
43
|
+
ext = os.path.splitext(target)[1].lower()
|
|
44
|
+
|
|
45
|
+
if ext == ".py":
|
|
46
|
+
handlers.handle_python(target, command, env, args)
|
|
47
|
+
elif ext in [".bat", ".cmd"]:
|
|
48
|
+
handlers.handle_batch(target, command, env, args)
|
|
49
|
+
elif ext in [".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".webp"]:
|
|
50
|
+
handlers.handle_image(target, command, env, args)
|
|
51
|
+
elif ext in [".js", ".ts", ".mjs", ".cjs"] or target.lower() in ["node", "npm", "npx", "node.exe", "npm.cmd", "npx.cmd"]:
|
|
52
|
+
handlers.handle_node(target, command, env, args)
|
|
53
|
+
elif ext == ".exe":
|
|
54
|
+
handlers.handle_exe(target, command, env, args)
|
|
55
|
+
else:
|
|
56
|
+
# Check if it's implicitly a python script via "python script.py"
|
|
57
|
+
if target.lower().endswith("python") or target.lower().endswith("python.exe"):
|
|
58
|
+
handlers.handle_python(target, command, env, args)
|
|
59
|
+
else:
|
|
60
|
+
handlers.handle_generic(target, command, env, args)
|
|
61
|
+
|
|
62
|
+
print("[h4-debug] Process finished. Dashboard will remain open until you exit (Ctrl+C).")
|
|
63
|
+
try:
|
|
64
|
+
while True:
|
|
65
|
+
time.sleep(1)
|
|
66
|
+
except KeyboardInterrupt:
|
|
67
|
+
print("[h4-debug] Shutting down.")
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
main()
|
|
71
|
+
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
import tempfile
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
from .interceptor import TelemetryClient
|
|
8
|
+
|
|
9
|
+
def _create_sitecustomize(mode, temp_dir):
|
|
10
|
+
sitecustomize_path = os.path.join(temp_dir, "sitecustomize.py")
|
|
11
|
+
script = f"""
|
|
12
|
+
import sys
|
|
13
|
+
import os
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
sys.path.remove(r"{temp_dir}")
|
|
17
|
+
except ValueError:
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
import h4_debug.interceptor
|
|
22
|
+
h4_debug.interceptor.start_interception(mode="{mode}")
|
|
23
|
+
except ImportError as e:
|
|
24
|
+
print(f"h4-debug: Failed to load interceptor: {{e}}", file=sys.stderr)
|
|
25
|
+
"""
|
|
26
|
+
with open(sitecustomize_path, "w", encoding="utf-8") as f:
|
|
27
|
+
f.write(script)
|
|
28
|
+
|
|
29
|
+
def handle_python(target, command, env, args):
|
|
30
|
+
print(f"[h4-debug] Handling as Python script")
|
|
31
|
+
temp_dir = tempfile.mkdtemp(prefix="h4_debug_")
|
|
32
|
+
_create_sitecustomize(args.mode, temp_dir)
|
|
33
|
+
|
|
34
|
+
if "PYTHONPATH" in env:
|
|
35
|
+
env["PYTHONPATH"] = f"{temp_dir}{os.pathsep}{env['PYTHONPATH']}"
|
|
36
|
+
else:
|
|
37
|
+
env["PYTHONPATH"] = temp_dir
|
|
38
|
+
|
|
39
|
+
env["H4_DEBUG_MODE"] = args.mode
|
|
40
|
+
env["H4_DEBUG_PORT"] = str(args.port)
|
|
41
|
+
|
|
42
|
+
print(f"[h4-debug] Launching process: {' '.join(command)}")
|
|
43
|
+
process = None
|
|
44
|
+
try:
|
|
45
|
+
process = subprocess.Popen(command, env=env)
|
|
46
|
+
process.wait()
|
|
47
|
+
except KeyboardInterrupt:
|
|
48
|
+
print("\n[h4-debug] Interrupted by user. Terminating process...")
|
|
49
|
+
if process:
|
|
50
|
+
process.terminate()
|
|
51
|
+
process.wait()
|
|
52
|
+
except Exception as e:
|
|
53
|
+
print(f"[h4-debug] Error running command: {e}")
|
|
54
|
+
|
|
55
|
+
def handle_batch(target, command, env, args):
|
|
56
|
+
print(f"[h4-debug] Handling as Batch script")
|
|
57
|
+
client = TelemetryClient(args.port)
|
|
58
|
+
time.sleep(0.5) # Wait for connection
|
|
59
|
+
|
|
60
|
+
# Read batch file and send content
|
|
61
|
+
try:
|
|
62
|
+
with open(target, 'r', encoding='utf-8', errors='replace') as f:
|
|
63
|
+
content = f.read()
|
|
64
|
+
client.send("File", "batch_content", {"file": target, "content": content})
|
|
65
|
+
except Exception as e:
|
|
66
|
+
client.send("System", "error", {"text": f"Failed to read batch file: {e}"})
|
|
67
|
+
|
|
68
|
+
# Prepare env for inner python calls
|
|
69
|
+
temp_dir = tempfile.mkdtemp(prefix="h4_debug_")
|
|
70
|
+
_create_sitecustomize(args.mode, temp_dir)
|
|
71
|
+
if "PYTHONPATH" in env:
|
|
72
|
+
env["PYTHONPATH"] = f"{temp_dir}{os.pathsep}{env['PYTHONPATH']}"
|
|
73
|
+
else:
|
|
74
|
+
env["PYTHONPATH"] = temp_dir
|
|
75
|
+
env["H4_DEBUG_MODE"] = args.mode
|
|
76
|
+
env["H4_DEBUG_PORT"] = str(args.port)
|
|
77
|
+
|
|
78
|
+
print(f"[h4-debug] Launching batch process: {' '.join(command)}")
|
|
79
|
+
process = None
|
|
80
|
+
try:
|
|
81
|
+
# Wrap command with cmd.exe /c if not already
|
|
82
|
+
if not command[0].lower().endswith('cmd.exe'):
|
|
83
|
+
cmd_args = ["cmd.exe", "/c"] + command
|
|
84
|
+
else:
|
|
85
|
+
cmd_args = command
|
|
86
|
+
|
|
87
|
+
process = subprocess.Popen(cmd_args, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1)
|
|
88
|
+
|
|
89
|
+
# Stream stdout and stderr
|
|
90
|
+
import threading
|
|
91
|
+
def stream_output(pipe, is_err):
|
|
92
|
+
for line in pipe:
|
|
93
|
+
client.send("Console", "stderr" if is_err else "stdout", {"text": line.rstrip()})
|
|
94
|
+
print(line, end='', file=sys.stderr if is_err else sys.stdout)
|
|
95
|
+
|
|
96
|
+
t1 = threading.Thread(target=stream_output, args=(process.stdout, False), daemon=True)
|
|
97
|
+
t2 = threading.Thread(target=stream_output, args=(process.stderr, True), daemon=True)
|
|
98
|
+
t1.start()
|
|
99
|
+
t2.start()
|
|
100
|
+
|
|
101
|
+
process.wait()
|
|
102
|
+
t1.join()
|
|
103
|
+
t2.join()
|
|
104
|
+
|
|
105
|
+
client.send("System", "info", {"text": f"Batch process finished with code {process.returncode}"})
|
|
106
|
+
except KeyboardInterrupt:
|
|
107
|
+
print("\n[h4-debug] Interrupted by user.")
|
|
108
|
+
if process:
|
|
109
|
+
process.terminate()
|
|
110
|
+
process.wait()
|
|
111
|
+
except Exception as e:
|
|
112
|
+
print(f"[h4-debug] Error running command: {e}")
|
|
113
|
+
|
|
114
|
+
def handle_image(target, command, env, args):
|
|
115
|
+
print(f"[h4-debug] Handling as Image file")
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
from PIL import Image
|
|
119
|
+
except ImportError:
|
|
120
|
+
print("[h4-debug] Pillow is not installed. EXIF extraction unavailable.")
|
|
121
|
+
handle_generic(target, command, env, args)
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
def cmd_handler(cmd):
|
|
125
|
+
if cmd.get("action") == "strip_exif":
|
|
126
|
+
try:
|
|
127
|
+
img = Image.open(target)
|
|
128
|
+
data = list(img.getdata())
|
|
129
|
+
image_without_exif = Image.new(img.mode, img.size)
|
|
130
|
+
image_without_exif.putdata(data)
|
|
131
|
+
|
|
132
|
+
name, ext = os.path.splitext(target)
|
|
133
|
+
new_target = f"{name}_stripped{ext}"
|
|
134
|
+
image_without_exif.save(new_target)
|
|
135
|
+
client.send("System", "info", {"text": f"Image metadata stripped and saved to: {new_target}"})
|
|
136
|
+
print(f"[h4-debug] Image stripped successfully: {new_target}")
|
|
137
|
+
except Exception as e:
|
|
138
|
+
client.send("System", "error", {"text": f"Failed to strip EXIF: {e}"})
|
|
139
|
+
return True
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
client = TelemetryClient(args.port, command_callback=cmd_handler)
|
|
143
|
+
time.sleep(0.5)
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
img = Image.open(target)
|
|
147
|
+
exif = img.getexif()
|
|
148
|
+
metadata = {
|
|
149
|
+
"format": img.format,
|
|
150
|
+
"mode": img.mode,
|
|
151
|
+
"size": img.size,
|
|
152
|
+
"exif": {k: str(v) for k, v in (exif.items() if exif else [])},
|
|
153
|
+
"file": target
|
|
154
|
+
}
|
|
155
|
+
client.send("Data", "image_metadata", metadata)
|
|
156
|
+
print(f"[h4-debug] Extracted metadata for {target}. Sent to dashboard.")
|
|
157
|
+
except Exception as e:
|
|
158
|
+
client.send("System", "error", {"text": f"Failed to read image metadata: {e}"})
|
|
159
|
+
print(f"[h4-debug] Error reading image: {e}")
|
|
160
|
+
|
|
161
|
+
print("[h4-debug] Image handler running. Dashboard is active. (Press Ctrl+C to stop)")
|
|
162
|
+
try:
|
|
163
|
+
while True:
|
|
164
|
+
time.sleep(1)
|
|
165
|
+
except KeyboardInterrupt:
|
|
166
|
+
print("\n[h4-debug] Exiting image handler.")
|
|
167
|
+
|
|
168
|
+
def handle_generic(target, command, env, args):
|
|
169
|
+
print(f"[h4-debug] Handling as generic command")
|
|
170
|
+
client = TelemetryClient(args.port)
|
|
171
|
+
time.sleep(0.5)
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
size = os.path.getsize(target)
|
|
175
|
+
client.send("System", "info", {"text": f"Generic file: {target} (Size: {size} bytes)"})
|
|
176
|
+
except Exception:
|
|
177
|
+
pass
|
|
178
|
+
|
|
179
|
+
process = None
|
|
180
|
+
try:
|
|
181
|
+
process = subprocess.Popen(command, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1)
|
|
182
|
+
|
|
183
|
+
import threading
|
|
184
|
+
def stream_output(pipe, is_err):
|
|
185
|
+
for line in pipe:
|
|
186
|
+
client.send("Console", "stderr" if is_err else "stdout", {"text": line.rstrip()})
|
|
187
|
+
print(line, end='', file=sys.stderr if is_err else sys.stdout)
|
|
188
|
+
|
|
189
|
+
t1 = threading.Thread(target=stream_output, args=(process.stdout, False), daemon=True)
|
|
190
|
+
t2 = threading.Thread(target=stream_output, args=(process.stderr, True), daemon=True)
|
|
191
|
+
t1.start()
|
|
192
|
+
t2.start()
|
|
193
|
+
|
|
194
|
+
process.wait()
|
|
195
|
+
t1.join()
|
|
196
|
+
t2.join()
|
|
197
|
+
|
|
198
|
+
client.send("System", "info", {"text": f"Generic process finished with code {process.returncode}"})
|
|
199
|
+
except KeyboardInterrupt:
|
|
200
|
+
if process:
|
|
201
|
+
process.terminate()
|
|
202
|
+
process.wait()
|
|
203
|
+
except Exception as e:
|
|
204
|
+
print(f"[h4-debug] Error running generic command: {e}")
|
|
205
|
+
|
|
206
|
+
def handle_node(target, command, env, args):
|
|
207
|
+
print(f"[h4-debug] Handling as Node.js script")
|
|
208
|
+
|
|
209
|
+
# Locate the interceptor script
|
|
210
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
211
|
+
interceptor_path = os.path.join(current_dir, "node_interceptor.js").replace("\\", "/")
|
|
212
|
+
|
|
213
|
+
# Format NODE_OPTIONS
|
|
214
|
+
node_opts = env.get("NODE_OPTIONS", "")
|
|
215
|
+
new_opt = f'--require "{interceptor_path}"'
|
|
216
|
+
env["NODE_OPTIONS"] = f"{new_opt} {node_opts}".strip()
|
|
217
|
+
|
|
218
|
+
env["H4_DEBUG_MODE"] = args.mode
|
|
219
|
+
env["H4_DEBUG_PORT"] = str(args.port)
|
|
220
|
+
|
|
221
|
+
process = None
|
|
222
|
+
try:
|
|
223
|
+
# If target is .js but command doesn't start with node, prepend node
|
|
224
|
+
if target.endswith('.js') and command[0] == target:
|
|
225
|
+
command = ["node"] + command
|
|
226
|
+
|
|
227
|
+
process = subprocess.Popen(command, env=env)
|
|
228
|
+
process.wait()
|
|
229
|
+
except KeyboardInterrupt:
|
|
230
|
+
if process:
|
|
231
|
+
process.terminate()
|
|
232
|
+
process.wait()
|
|
233
|
+
except Exception as e:
|
|
234
|
+
print(f"[h4-debug] Error running node command: {e}")
|
|
235
|
+
|
|
236
|
+
def handle_exe(target, command, env, args):
|
|
237
|
+
print(f"[h4-debug] Handling as Native Executable (using ctypes Windows Debugger)")
|
|
238
|
+
client = TelemetryClient(args.port)
|
|
239
|
+
time.sleep(0.5)
|
|
240
|
+
|
|
241
|
+
env["H4_DEBUG_MODE"] = args.mode
|
|
242
|
+
env["H4_DEBUG_PORT"] = str(args.port)
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
from h4_debug.win_debugger import debug_process
|
|
246
|
+
client.send("System", "info", {"text": f"Starting native debug wrapper for {target}"})
|
|
247
|
+
debug_process(command, client)
|
|
248
|
+
except KeyboardInterrupt:
|
|
249
|
+
pass
|
|
250
|
+
except Exception as e:
|
|
251
|
+
print(f"[h4-debug] Error running EXE command: {e}")
|
|
252
|
+
|
|
@@ -18,8 +18,9 @@ _original_socket = socket.socket
|
|
|
18
18
|
_original_print = builtins.print
|
|
19
19
|
|
|
20
20
|
class TelemetryClient:
|
|
21
|
-
def __init__(self, port):
|
|
21
|
+
def __init__(self, port, command_callback=None):
|
|
22
22
|
self.port = port
|
|
23
|
+
self.command_callback = command_callback
|
|
23
24
|
self.queue = queue.Queue()
|
|
24
25
|
self.buffer = ""
|
|
25
26
|
self.thread = threading.Thread(target=self._run, daemon=True)
|
|
@@ -64,6 +65,9 @@ class TelemetryClient:
|
|
|
64
65
|
try:
|
|
65
66
|
cmd = json.loads(cmd_data)
|
|
66
67
|
|
|
68
|
+
if self.command_callback and self.command_callback(cmd):
|
|
69
|
+
return
|
|
70
|
+
|
|
67
71
|
if cmd.get("action") == "set_mode":
|
|
68
72
|
global _mode
|
|
69
73
|
_mode = cmd.get("mode", "Normal")
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
const net = require('net');
|
|
2
|
+
const http = require('http');
|
|
3
|
+
const https = require('https');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
|
|
6
|
+
const mode = process.env.H4_DEBUG_MODE || 'Normal';
|
|
7
|
+
const port = parseInt(process.env.H4_DEBUG_PORT || '8008', 10) + 1;
|
|
8
|
+
const isTelemetryThread = false;
|
|
9
|
+
|
|
10
|
+
const messageQueue = [];
|
|
11
|
+
let client = null;
|
|
12
|
+
let connected = false;
|
|
13
|
+
|
|
14
|
+
function connectTelemetry() {
|
|
15
|
+
client = net.createConnection({ port: port, host: '127.0.0.1' }, () => {
|
|
16
|
+
connected = true;
|
|
17
|
+
sendQueue();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
client.on('error', () => {
|
|
21
|
+
connected = false;
|
|
22
|
+
setTimeout(connectTelemetry, 1000);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
client.on('close', () => {
|
|
26
|
+
connected = false;
|
|
27
|
+
setTimeout(connectTelemetry, 1000);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function sendQueue() {
|
|
32
|
+
if (!connected || !client) return;
|
|
33
|
+
while (messageQueue.length > 0) {
|
|
34
|
+
const msg = messageQueue.shift();
|
|
35
|
+
try {
|
|
36
|
+
client.write(JSON.stringify(msg) + '\n');
|
|
37
|
+
} catch (e) {
|
|
38
|
+
messageQueue.unshift(msg);
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function sendEvent(module, type, data) {
|
|
45
|
+
if (mode === 'Normal' && module !== 'System') return; // Do not send if normal, unless system init
|
|
46
|
+
const msg = {
|
|
47
|
+
timestamp: new Date().toISOString(),
|
|
48
|
+
module: module,
|
|
49
|
+
type: type,
|
|
50
|
+
data: data,
|
|
51
|
+
thread: 'Main'
|
|
52
|
+
};
|
|
53
|
+
messageQueue.push(msg);
|
|
54
|
+
sendQueue();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Start connection
|
|
58
|
+
connectTelemetry();
|
|
59
|
+
|
|
60
|
+
sendEvent('System', 'init', {
|
|
61
|
+
mode: mode,
|
|
62
|
+
pid: process.pid,
|
|
63
|
+
language: 'node.js',
|
|
64
|
+
version: process.version
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Patch Console
|
|
68
|
+
const originalLog = console.log;
|
|
69
|
+
const originalError = console.error;
|
|
70
|
+
const originalWarn = console.warn;
|
|
71
|
+
|
|
72
|
+
console.log = function(...args) {
|
|
73
|
+
sendEvent('Console', 'stdout', { text: args.join(' ') });
|
|
74
|
+
originalLog.apply(console, args);
|
|
75
|
+
};
|
|
76
|
+
console.error = function(...args) {
|
|
77
|
+
sendEvent('Console', 'stderr', { text: args.join(' ') });
|
|
78
|
+
originalError.apply(console, args);
|
|
79
|
+
};
|
|
80
|
+
console.warn = function(...args) {
|
|
81
|
+
sendEvent('Console', 'stdout', { text: '[WARN] ' + args.join(' ') });
|
|
82
|
+
originalWarn.apply(console, args);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Patch HTTP
|
|
86
|
+
const originalHttpRequest = http.request;
|
|
87
|
+
http.request = function(...args) {
|
|
88
|
+
let url = typeof args[0] === 'string' ? args[0] : (args[0] && args[0].href) ? args[0].href : JSON.stringify(args[0]);
|
|
89
|
+
sendEvent('Network', 'connect', { address: url });
|
|
90
|
+
return originalHttpRequest.apply(http, args);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const originalHttpsRequest = https.request;
|
|
94
|
+
https.request = function(...args) {
|
|
95
|
+
let url = typeof args[0] === 'string' ? args[0] : (args[0] && args[0].href) ? args[0].href : JSON.stringify(args[0]);
|
|
96
|
+
sendEvent('Network', 'connect', { address: url });
|
|
97
|
+
return originalHttpsRequest.apply(https, args);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Patch File System (read sync as example)
|
|
101
|
+
const originalReadFileSync = fs.readFileSync;
|
|
102
|
+
fs.readFileSync = function(...args) {
|
|
103
|
+
sendEvent('Disk', 'open', { file: args[0], mode: 'r' });
|
|
104
|
+
return originalReadFileSync.apply(fs, args);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// Unhandled Rejection
|
|
108
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
109
|
+
sendEvent('Execution', 'exception', {
|
|
110
|
+
type: 'UnhandledRejection',
|
|
111
|
+
value: String(reason),
|
|
112
|
+
file: 'unknown',
|
|
113
|
+
line: 0
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
process.on('uncaughtException', (err) => {
|
|
118
|
+
sendEvent('Execution', 'exception', {
|
|
119
|
+
type: err.name,
|
|
120
|
+
value: err.message,
|
|
121
|
+
file: err.stack,
|
|
122
|
+
line: 0
|
|
123
|
+
});
|
|
124
|
+
// Rethrow to maintain standard behavior
|
|
125
|
+
throw err;
|
|
126
|
+
});
|
|
@@ -77,6 +77,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
77
77
|
renderBasicLog(containers.disk, log, timeStr);
|
|
78
78
|
} else if (log.module === 'Execution') {
|
|
79
79
|
renderBasicLog(containers.execution, log, timeStr);
|
|
80
|
+
} else if (log.module === 'Data' && log.type === 'image_metadata') {
|
|
81
|
+
renderImageMetadata(containers.disk, log, timeStr);
|
|
82
|
+
// Also show a brief info in console
|
|
83
|
+
renderBasicLog(containers.console, {module: 'System', type: 'info', data: {text: `Image metadata loaded for ${log.data.file}. Check Disk tab.`}}, timeStr, 'system');
|
|
80
84
|
} else if (log.module === 'Console') {
|
|
81
85
|
if (log.type === 'eval_result') {
|
|
82
86
|
renderEvalResult(containers.console, log, timeStr);
|
|
@@ -94,6 +98,43 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
94
98
|
}
|
|
95
99
|
}
|
|
96
100
|
|
|
101
|
+
function renderImageMetadata(container, log, timeStr) {
|
|
102
|
+
const el = document.createElement('div');
|
|
103
|
+
el.className = 'log-entry image-metadata';
|
|
104
|
+
|
|
105
|
+
let html = `<h3>Image: ${log.data.file}</h3>`;
|
|
106
|
+
html += `<div><strong>Format:</strong> ${log.data.format} | <strong>Size:</strong> ${log.data.size[0]}x${log.data.size[1]} | <strong>Mode:</strong> ${log.data.mode}</div>`;
|
|
107
|
+
|
|
108
|
+
const exifCount = Object.keys(log.data.exif).length;
|
|
109
|
+
if (exifCount > 0) {
|
|
110
|
+
html += `<h4>EXIF Data (${exifCount} entries)</h4>`;
|
|
111
|
+
html += `<pre style="max-height: 200px; overflow-y: auto; background: var(--bg-dark); padding: 5px; font-size: 0.9em;">`;
|
|
112
|
+
for (let [k, v] of Object.entries(log.data.exif)) {
|
|
113
|
+
html += `${k}: ${v}\n`;
|
|
114
|
+
}
|
|
115
|
+
html += `</pre>`;
|
|
116
|
+
html += `<button class="btn-strip-exif" style="margin-top: 10px; padding: 5px 10px; background: #e74c3c; color: white; border: none; border-radius: 4px; cursor: pointer;">Strip EXIF Metadata</button>`;
|
|
117
|
+
} else {
|
|
118
|
+
html += `<h4>No EXIF Data found.</h4>`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
el.innerHTML = html;
|
|
122
|
+
|
|
123
|
+
const btn = el.querySelector('.btn-strip-exif');
|
|
124
|
+
if (btn) {
|
|
125
|
+
btn.addEventListener('click', () => {
|
|
126
|
+
if(ws && ws.readyState === WebSocket.OPEN) {
|
|
127
|
+
ws.send(JSON.stringify({action: 'strip_exif', file: log.data.file}));
|
|
128
|
+
btn.textContent = "Stripping...";
|
|
129
|
+
btn.disabled = true;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
container.appendChild(el);
|
|
135
|
+
scrollToBottom(container);
|
|
136
|
+
}
|
|
137
|
+
|
|
97
138
|
function renderBasicLog(container, log, timeStr, customClass='') {
|
|
98
139
|
const el = document.createElement('div');
|
|
99
140
|
el.className = `log-entry ${customClass}`;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import ctypes
|
|
2
|
+
from ctypes import wintypes
|
|
3
|
+
import time
|
|
4
|
+
import struct
|
|
5
|
+
import sys
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
|
|
9
|
+
|
|
10
|
+
DEBUG_PROCESS = 0x00000001
|
|
11
|
+
DEBUG_ONLY_THIS_PROCESS = 0x00000002
|
|
12
|
+
DBG_CONTINUE = 0x00010002
|
|
13
|
+
DBG_EXCEPTION_NOT_HANDLED = 0x80010001
|
|
14
|
+
|
|
15
|
+
# Event Codes
|
|
16
|
+
EXCEPTION_DEBUG_EVENT = 1
|
|
17
|
+
CREATE_THREAD_DEBUG_EVENT = 2
|
|
18
|
+
CREATE_PROCESS_DEBUG_EVENT = 3
|
|
19
|
+
EXIT_THREAD_DEBUG_EVENT = 4
|
|
20
|
+
EXIT_PROCESS_DEBUG_EVENT = 5
|
|
21
|
+
LOAD_DLL_DEBUG_EVENT = 6
|
|
22
|
+
UNLOAD_DLL_DEBUG_EVENT = 7
|
|
23
|
+
OUTPUT_DEBUG_STRING_EVENT = 8
|
|
24
|
+
RIP_EVENT = 9
|
|
25
|
+
|
|
26
|
+
class STARTUPINFO(ctypes.Structure):
|
|
27
|
+
_fields_ = [
|
|
28
|
+
("cb", wintypes.DWORD),
|
|
29
|
+
("lpReserved", wintypes.LPWSTR),
|
|
30
|
+
("lpDesktop", wintypes.LPWSTR),
|
|
31
|
+
("lpTitle", wintypes.LPWSTR),
|
|
32
|
+
("dwX", wintypes.DWORD),
|
|
33
|
+
("dwY", wintypes.DWORD),
|
|
34
|
+
("dwXSize", wintypes.DWORD),
|
|
35
|
+
("dwYSize", wintypes.DWORD),
|
|
36
|
+
("dwXCountChars", wintypes.DWORD),
|
|
37
|
+
("dwYCountChars", wintypes.DWORD),
|
|
38
|
+
("dwFillAttribute", wintypes.DWORD),
|
|
39
|
+
("dwFlags", wintypes.DWORD),
|
|
40
|
+
("wShowWindow", wintypes.WORD),
|
|
41
|
+
("cbReserved2", wintypes.WORD),
|
|
42
|
+
("lpReserved2", ctypes.POINTER(wintypes.BYTE)),
|
|
43
|
+
("hStdInput", wintypes.HANDLE),
|
|
44
|
+
("hStdOutput", wintypes.HANDLE),
|
|
45
|
+
("hStdError", wintypes.HANDLE),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
class PROCESS_INFORMATION(ctypes.Structure):
|
|
49
|
+
_fields_ = [
|
|
50
|
+
("hProcess", wintypes.HANDLE),
|
|
51
|
+
("hThread", wintypes.HANDLE),
|
|
52
|
+
("dwProcessId", wintypes.DWORD),
|
|
53
|
+
("dwThreadId", wintypes.DWORD),
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
class EXCEPTION_RECORD(ctypes.Structure):
|
|
57
|
+
pass
|
|
58
|
+
EXCEPTION_RECORD._fields_ = [
|
|
59
|
+
("ExceptionCode", wintypes.DWORD),
|
|
60
|
+
("ExceptionFlags", wintypes.DWORD),
|
|
61
|
+
("ExceptionRecord", ctypes.POINTER(EXCEPTION_RECORD)),
|
|
62
|
+
("ExceptionAddress", ctypes.c_void_p),
|
|
63
|
+
("NumberParameters", wintypes.DWORD),
|
|
64
|
+
("ExceptionInformation", ctypes.c_void_p * 15),
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
class EXCEPTION_DEBUG_INFO(ctypes.Structure):
|
|
68
|
+
_fields_ = [
|
|
69
|
+
("ExceptionRecord", EXCEPTION_RECORD),
|
|
70
|
+
("dwFirstChance", wintypes.DWORD),
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
class CREATE_THREAD_DEBUG_INFO(ctypes.Structure):
|
|
74
|
+
_fields_ = [
|
|
75
|
+
("hThread", wintypes.HANDLE),
|
|
76
|
+
("lpThreadLocalBase", ctypes.c_void_p),
|
|
77
|
+
("lpStartAddress", ctypes.c_void_p),
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
class CREATE_PROCESS_DEBUG_INFO(ctypes.Structure):
|
|
81
|
+
_fields_ = [
|
|
82
|
+
("hFile", wintypes.HANDLE),
|
|
83
|
+
("hProcess", wintypes.HANDLE),
|
|
84
|
+
("hThread", wintypes.HANDLE),
|
|
85
|
+
("lpBaseOfImage", ctypes.c_void_p),
|
|
86
|
+
("dwDebugInfoFileOffset", wintypes.DWORD),
|
|
87
|
+
("nDebugInfoSize", wintypes.DWORD),
|
|
88
|
+
("lpThreadLocalBase", ctypes.c_void_p),
|
|
89
|
+
("lpStartAddress", ctypes.c_void_p),
|
|
90
|
+
("lpImageName", ctypes.c_void_p),
|
|
91
|
+
("fUnicode", wintypes.WORD),
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
class EXIT_THREAD_DEBUG_INFO(ctypes.Structure):
|
|
95
|
+
_fields_ = [
|
|
96
|
+
("dwExitCode", wintypes.DWORD),
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
class EXIT_PROCESS_DEBUG_INFO(ctypes.Structure):
|
|
100
|
+
_fields_ = [
|
|
101
|
+
("dwExitCode", wintypes.DWORD),
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
class LOAD_DLL_DEBUG_INFO(ctypes.Structure):
|
|
105
|
+
_fields_ = [
|
|
106
|
+
("hFile", wintypes.HANDLE),
|
|
107
|
+
("lpBaseOfDll", ctypes.c_void_p),
|
|
108
|
+
("dwDebugInfoFileOffset", wintypes.DWORD),
|
|
109
|
+
("nDebugInfoSize", wintypes.DWORD),
|
|
110
|
+
("lpImageName", ctypes.c_void_p),
|
|
111
|
+
("fUnicode", wintypes.WORD),
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
class UNLOAD_DLL_DEBUG_INFO(ctypes.Structure):
|
|
115
|
+
_fields_ = [
|
|
116
|
+
("lpBaseOfDll", ctypes.c_void_p),
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
class OUTPUT_DEBUG_STRING_INFO(ctypes.Structure):
|
|
120
|
+
_fields_ = [
|
|
121
|
+
("lpDebugStringData", ctypes.c_void_p),
|
|
122
|
+
("fUnicode", wintypes.WORD),
|
|
123
|
+
("nDebugStringLength", wintypes.WORD),
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
class RIP_INFO(ctypes.Structure):
|
|
127
|
+
_fields_ = [
|
|
128
|
+
("dwError", wintypes.DWORD),
|
|
129
|
+
("dwType", wintypes.DWORD),
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
class DEBUG_EVENT_UNION(ctypes.Union):
|
|
133
|
+
_fields_ = [
|
|
134
|
+
("Exception", EXCEPTION_DEBUG_INFO),
|
|
135
|
+
("CreateThread", CREATE_THREAD_DEBUG_INFO),
|
|
136
|
+
("CreateProcessInfo", CREATE_PROCESS_DEBUG_INFO),
|
|
137
|
+
("ExitThread", EXIT_THREAD_DEBUG_INFO),
|
|
138
|
+
("ExitProcess", EXIT_PROCESS_DEBUG_INFO),
|
|
139
|
+
("LoadDll", LOAD_DLL_DEBUG_INFO),
|
|
140
|
+
("UnloadDll", UNLOAD_DLL_DEBUG_INFO),
|
|
141
|
+
("DebugString", OUTPUT_DEBUG_STRING_INFO),
|
|
142
|
+
("RipInfo", RIP_INFO),
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
class DEBUG_EVENT(ctypes.Structure):
|
|
146
|
+
_fields_ = [
|
|
147
|
+
("dwDebugEventCode", wintypes.DWORD),
|
|
148
|
+
("dwProcessId", wintypes.DWORD),
|
|
149
|
+
("dwThreadId", wintypes.DWORD),
|
|
150
|
+
("u", DEBUG_EVENT_UNION),
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
def read_process_memory(hProcess, address, size):
|
|
154
|
+
buffer = ctypes.create_string_buffer(size)
|
|
155
|
+
bytesRead = ctypes.c_size_t()
|
|
156
|
+
if kernel32.ReadProcessMemory(hProcess, ctypes.c_void_p(address), buffer, size, ctypes.byref(bytesRead)):
|
|
157
|
+
return buffer.raw[:bytesRead.value]
|
|
158
|
+
return b""
|
|
159
|
+
|
|
160
|
+
def debug_process(command, client):
|
|
161
|
+
si = STARTUPINFO()
|
|
162
|
+
si.cb = ctypes.sizeof(si)
|
|
163
|
+
pi = PROCESS_INFORMATION()
|
|
164
|
+
|
|
165
|
+
creation_flags = DEBUG_PROCESS
|
|
166
|
+
|
|
167
|
+
# Convert command list to string for Windows
|
|
168
|
+
cmd_str = " ".join(f'"{c}"' if " " in c else c for c in command)
|
|
169
|
+
|
|
170
|
+
if not kernel32.CreateProcessW(
|
|
171
|
+
None, ctypes.c_wchar_p(cmd_str), None, None, False,
|
|
172
|
+
creation_flags, None, None, ctypes.byref(si), ctypes.byref(pi)):
|
|
173
|
+
client.send("System", "error", {"text": f"Failed to CreateProcess: {ctypes.GetLastError()}"})
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
process_handle = None
|
|
177
|
+
debug_event = DEBUG_EVENT()
|
|
178
|
+
|
|
179
|
+
while True:
|
|
180
|
+
if not kernel32.WaitForDebugEvent(ctypes.byref(debug_event), 1000): # 1 sec timeout to yield
|
|
181
|
+
# Check if process exited
|
|
182
|
+
exit_code = wintypes.DWORD()
|
|
183
|
+
if kernel32.GetExitCodeProcess(pi.hProcess, ctypes.byref(exit_code)) and exit_code.value != 259: # STILL_ACTIVE
|
|
184
|
+
break
|
|
185
|
+
continue
|
|
186
|
+
|
|
187
|
+
continue_status = DBG_CONTINUE
|
|
188
|
+
|
|
189
|
+
if debug_event.dwDebugEventCode == CREATE_PROCESS_DEBUG_EVENT:
|
|
190
|
+
process_handle = debug_event.u.CreateProcessInfo.hProcess
|
|
191
|
+
client.send("System", "info", {"text": f"Process Created: PID {debug_event.dwProcessId}"})
|
|
192
|
+
if debug_event.u.CreateProcessInfo.hFile:
|
|
193
|
+
kernel32.CloseHandle(debug_event.u.CreateProcessInfo.hFile)
|
|
194
|
+
|
|
195
|
+
elif debug_event.dwDebugEventCode == LOAD_DLL_DEBUG_EVENT:
|
|
196
|
+
if debug_event.u.LoadDll.hFile:
|
|
197
|
+
kernel32.CloseHandle(debug_event.u.LoadDll.hFile)
|
|
198
|
+
# Address resolution of DLL names can be complex in Win32, so we just log the event natively.
|
|
199
|
+
# Real debuggers use GetMappedFileName on the lpBaseOfDll.
|
|
200
|
+
pass # Keep output clean unless we resolve name
|
|
201
|
+
|
|
202
|
+
elif debug_event.dwDebugEventCode == OUTPUT_DEBUG_STRING_EVENT:
|
|
203
|
+
if process_handle:
|
|
204
|
+
info = debug_event.u.DebugString
|
|
205
|
+
data = read_process_memory(process_handle, info.lpDebugStringData, info.nDebugStringLength * (2 if info.fUnicode else 1))
|
|
206
|
+
if data:
|
|
207
|
+
text = data.decode('utf-16' if info.fUnicode else 'ansi', 'replace').strip('\x00\r\n')
|
|
208
|
+
if text:
|
|
209
|
+
client.send("Console", "stdout", {"text": f"[DEBUG] {text}"})
|
|
210
|
+
|
|
211
|
+
elif debug_event.dwDebugEventCode == EXCEPTION_DEBUG_EVENT:
|
|
212
|
+
exc = debug_event.u.Exception.ExceptionRecord.ExceptionCode
|
|
213
|
+
# Ignore standard breakpoint exceptions (0x80000003)
|
|
214
|
+
if exc != 0x80000003:
|
|
215
|
+
# Log exception
|
|
216
|
+
continue_status = DBG_EXCEPTION_NOT_HANDLED
|
|
217
|
+
|
|
218
|
+
elif debug_event.dwDebugEventCode == EXIT_PROCESS_DEBUG_EVENT:
|
|
219
|
+
client.send("System", "info", {"text": f"Process Exited with code {debug_event.u.ExitProcess.dwExitCode}"})
|
|
220
|
+
kernel32.ContinueDebugEvent(debug_event.dwProcessId, debug_event.dwThreadId, continue_status)
|
|
221
|
+
break
|
|
222
|
+
|
|
223
|
+
kernel32.ContinueDebugEvent(debug_event.dwProcessId, debug_event.dwThreadId, continue_status)
|
|
224
|
+
|
|
225
|
+
if process_handle:
|
|
226
|
+
kernel32.CloseHandle(pi.hThread)
|
|
227
|
+
kernel32.CloseHandle(pi.hProcess)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: h4-debug
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Advanced application proxy debugger and F12-style web console
|
|
5
5
|
Author-email: h4 <h4@example.com>
|
|
6
6
|
Requires-Python: >=3.8
|
|
@@ -8,6 +8,7 @@ Description-Content-Type: text/markdown
|
|
|
8
8
|
Requires-Dist: fastapi>=0.100
|
|
9
9
|
Requires-Dist: uvicorn[standard]>=0.20
|
|
10
10
|
Requires-Dist: websockets>=11.0
|
|
11
|
+
Requires-Dist: Pillow>=10.0.0
|
|
11
12
|
|
|
12
13
|
# h4-debug
|
|
13
14
|
|
|
@@ -2,8 +2,11 @@ README.md
|
|
|
2
2
|
pyproject.toml
|
|
3
3
|
h4_debug/__init__.py
|
|
4
4
|
h4_debug/cli.py
|
|
5
|
+
h4_debug/handlers.py
|
|
5
6
|
h4_debug/interceptor.py
|
|
7
|
+
h4_debug/node_interceptor.js
|
|
6
8
|
h4_debug/server.py
|
|
9
|
+
h4_debug/win_debugger.py
|
|
7
10
|
h4_debug.egg-info/PKG-INFO
|
|
8
11
|
h4_debug.egg-info/SOURCES.txt
|
|
9
12
|
h4_debug.egg-info/dependency_links.txt
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "h4-debug"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Advanced application proxy debugger and F12-style web console"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -15,10 +15,11 @@ dependencies = [
|
|
|
15
15
|
"fastapi>=0.100",
|
|
16
16
|
"uvicorn[standard]>=0.20",
|
|
17
17
|
"websockets>=11.0",
|
|
18
|
+
"Pillow>=10.0.0",
|
|
18
19
|
]
|
|
19
20
|
|
|
20
21
|
[project.scripts]
|
|
21
22
|
h4-debug = "h4_debug.cli:main"
|
|
22
23
|
|
|
23
24
|
[tool.setuptools.package-data]
|
|
24
|
-
h4_debug = ["static/*"]
|
|
25
|
+
"h4_debug" = ["static/*", "*.js"]
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.4"
|
h4_debug-0.1.4/h4_debug/cli.py
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import argparse
|
|
2
|
-
import os
|
|
3
|
-
import subprocess
|
|
4
|
-
import sys
|
|
5
|
-
import tempfile
|
|
6
|
-
import threading
|
|
7
|
-
import time
|
|
8
|
-
import webbrowser
|
|
9
|
-
import pathlib
|
|
10
|
-
|
|
11
|
-
from . import server
|
|
12
|
-
|
|
13
|
-
def create_sitecustomize(mode):
|
|
14
|
-
temp_dir = tempfile.mkdtemp(prefix="h4_debug_")
|
|
15
|
-
sitecustomize_path = os.path.join(temp_dir, "sitecustomize.py")
|
|
16
|
-
|
|
17
|
-
# We will write a sitecustomize.py that imports our interceptor
|
|
18
|
-
# and initializes it with the selected mode.
|
|
19
|
-
script = f"""
|
|
20
|
-
import sys
|
|
21
|
-
import os
|
|
22
|
-
|
|
23
|
-
# Remove this temp dir from sys.path so we don't mess with other imports too much
|
|
24
|
-
try:
|
|
25
|
-
sys.path.remove(r"{temp_dir}")
|
|
26
|
-
except ValueError:
|
|
27
|
-
pass
|
|
28
|
-
|
|
29
|
-
try:
|
|
30
|
-
import h4_debug.interceptor
|
|
31
|
-
h4_debug.interceptor.start_interception(mode="{mode}")
|
|
32
|
-
except ImportError as e:
|
|
33
|
-
print(f"h4-debug: Failed to load interceptor: {{e}}", file=sys.stderr)
|
|
34
|
-
"""
|
|
35
|
-
with open(sitecustomize_path, "w", encoding="utf-8") as f:
|
|
36
|
-
f.write(script)
|
|
37
|
-
|
|
38
|
-
return temp_dir
|
|
39
|
-
|
|
40
|
-
def main():
|
|
41
|
-
parser = argparse.ArgumentParser(description="h4-debug: Advanced application proxy debugger", usage="h4-debug [--mode MODE] command ...")
|
|
42
|
-
parser.add_argument("--mode", choices=["Normal", "Trace", "Full"], default="Normal", help="Debugging mode")
|
|
43
|
-
parser.add_argument("--port", type=int, default=8008, help="Port for the web dashboard")
|
|
44
|
-
parser.add_argument("command", nargs=argparse.REMAINDER, help="Command to run")
|
|
45
|
-
|
|
46
|
-
args = parser.parse_args()
|
|
47
|
-
|
|
48
|
-
if not args.command:
|
|
49
|
-
parser.print_help()
|
|
50
|
-
sys.exit(1)
|
|
51
|
-
|
|
52
|
-
command = args.command
|
|
53
|
-
# argparse puts '--' in the remainder if it's there
|
|
54
|
-
if command[0] == "--":
|
|
55
|
-
command = command[1:]
|
|
56
|
-
|
|
57
|
-
# 1. Start the web server in a background thread
|
|
58
|
-
print(f"[h4-debug] Starting dashboard on http://localhost:{args.port} ...")
|
|
59
|
-
server_thread = threading.Thread(target=server.run_server, args=(args.port,), daemon=True)
|
|
60
|
-
server_thread.start()
|
|
61
|
-
|
|
62
|
-
# Give server a moment to start
|
|
63
|
-
time.sleep(1.0)
|
|
64
|
-
|
|
65
|
-
# Open dashboard in browser
|
|
66
|
-
webbrowser.open(f"http://localhost:{args.port}")
|
|
67
|
-
|
|
68
|
-
# 2. Setup interception environment
|
|
69
|
-
env = os.environ.copy()
|
|
70
|
-
|
|
71
|
-
# Python interception via sitecustomize
|
|
72
|
-
sitecustomize_dir = create_sitecustomize(args.mode)
|
|
73
|
-
|
|
74
|
-
if "PYTHONPATH" in env:
|
|
75
|
-
env["PYTHONPATH"] = f"{sitecustomize_dir}{os.pathsep}{env['PYTHONPATH']}"
|
|
76
|
-
else:
|
|
77
|
-
env["PYTHONPATH"] = sitecustomize_dir
|
|
78
|
-
|
|
79
|
-
env["H4_DEBUG_MODE"] = args.mode
|
|
80
|
-
env["H4_DEBUG_PORT"] = str(args.port)
|
|
81
|
-
|
|
82
|
-
# 3. Launch the target process
|
|
83
|
-
print(f"[h4-debug] Launching process: {' '.join(command)}")
|
|
84
|
-
try:
|
|
85
|
-
process = subprocess.Popen(command, env=env)
|
|
86
|
-
process.wait()
|
|
87
|
-
except KeyboardInterrupt:
|
|
88
|
-
print("\n[h4-debug] Interrupted by user. Terminating process...")
|
|
89
|
-
if process:
|
|
90
|
-
process.terminate()
|
|
91
|
-
process.wait()
|
|
92
|
-
except Exception as e:
|
|
93
|
-
print(f"[h4-debug] Error running command: {e}")
|
|
94
|
-
finally:
|
|
95
|
-
print("[h4-debug] Process finished. Dashboard will remain open until you exit (Ctrl+C).")
|
|
96
|
-
try:
|
|
97
|
-
while True:
|
|
98
|
-
time.sleep(1)
|
|
99
|
-
except KeyboardInterrupt:
|
|
100
|
-
print("[h4-debug] Shutting down.")
|
|
101
|
-
|
|
102
|
-
if __name__ == "__main__":
|
|
103
|
-
main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|