httpdbg 2.3.0__tar.gz → 2.4.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.
- {httpdbg-2.3.0 → httpdbg-2.4.0}/PKG-INFO +1 -1
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/__init__.py +1 -1
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/export.py +2 -5
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/external.py +50 -7
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/record.py +62 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/recordhttp1.py +32 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/recordhttp2.py +40 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/template_sitecustomize.py +39 -10
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/initiator.py +22 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/records.py +42 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/utils.py +4 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/app.py +2 -10
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg.egg-info/PKG-INFO +1 -1
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg.egg-info/SOURCES.txt +0 -1
- httpdbg-2.3.0/httpdbg/webapp/api.py +0 -110
- {httpdbg-2.3.0 → httpdbg-2.4.0}/LICENSE +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/README.md +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/__main__.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/args.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/env.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/exception.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/__init__.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/aiohttp.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/all.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/fastapi.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/flask.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/generic.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/h2.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/http.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/httpx.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/pytest.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/requests.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/socket.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/starlette.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/unittest.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/urllib3.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/utils.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/uvicorn.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/http_status_code.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/log.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/mode_console.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/mode_module.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/mode_script.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/preview.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/server.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/__init__.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/api.js +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/column.css +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/configuration.css +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/configuration.js +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/detail.css +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/favicon.ico +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/filter.js +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/chevron-direction-top-outline-icon.svg +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/download-install-line-icon.svg +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/engine-gears-icon.svg +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/eye-blind-icon.svg +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/eye-icon.svg +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/files-papers-outline-icon.svg +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/filter-outline-icon.svg +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/lock-line-icon.svg +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/lock-open-line-icon.svg +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/math-multiplication-icon.svg +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/push-pin-green-icon.svg +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/question-mark-round-line-icon.svg +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/recycle-bin-line-icon.svg +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/wait-sandclock-icon.svg +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/index.htm +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/main.js +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/mustache.min.js +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/render.js +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/style.css +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/table.css +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg.egg-info/dependency_links.txt +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg.egg-info/entry_points.txt +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg.egg-info/top_level.txt +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/pyproject.toml +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/setup.cfg +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_api.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_cli.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_context_manager.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_hook_aiohttp.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_hook_h2.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_hook_httpx.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_hook_pytest.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_hook_requests.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_hook_unittest.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_hook_urllib3.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_initiator.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_mode_console.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_mode_module_pytest.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_mode_script.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_multiprocess.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_preview.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_pyhttpdbg_server.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_server.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_server_hook_fastapi.py +0 -0
- {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_server_hook_flask.py +0 -0
|
@@ -6,7 +6,6 @@ import xml.etree.ElementTree as ET
|
|
|
6
6
|
|
|
7
7
|
import httpdbg
|
|
8
8
|
from httpdbg import HTTPRecords
|
|
9
|
-
from httpdbg.webapp.api import RequestListPayload, RequestPayload
|
|
10
9
|
|
|
11
10
|
|
|
12
11
|
def generate_html(records: HTTPRecords, for_export: bool = True) -> str:
|
|
@@ -67,12 +66,10 @@ def generate_html(records: HTTPRecords, for_export: bool = True) -> str:
|
|
|
67
66
|
|
|
68
67
|
# static export of the requests data
|
|
69
68
|
if for_export:
|
|
70
|
-
static_all_requests =
|
|
71
|
-
records, cls=RequestListPayload, ensure_ascii=False
|
|
72
|
-
)
|
|
69
|
+
static_all_requests = records.payload.decode("ascii")
|
|
73
70
|
map_requests: dict[str, object] = dict()
|
|
74
71
|
for record in records:
|
|
75
|
-
map_requests[record.id] = json.loads(
|
|
72
|
+
map_requests[record.id] = json.loads(record.payload.decode("ascii"))
|
|
76
73
|
static_requests: str = json.dumps(map_requests, ensure_ascii=False)
|
|
77
74
|
|
|
78
75
|
def safe_for_script_tag(s: str) -> str:
|
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
from collections.abc import Generator
|
|
2
2
|
from contextlib import contextmanager
|
|
3
|
+
import json
|
|
3
4
|
import glob
|
|
4
5
|
import os
|
|
5
|
-
import pickle
|
|
6
6
|
import tempfile
|
|
7
7
|
import time
|
|
8
8
|
import threading
|
|
9
9
|
|
|
10
10
|
from httpdbg.env import HTTPDBG_MULTIPROCESS_DIR
|
|
11
|
+
from httpdbg.hooks.recordhttp1 import HTTP1Record
|
|
12
|
+
from httpdbg.hooks.recordhttp2 import HTTP2Record
|
|
13
|
+
from httpdbg.initiator import Group
|
|
14
|
+
from httpdbg.initiator import Initiator
|
|
11
15
|
from httpdbg.log import logger
|
|
12
16
|
from httpdbg.records import HTTPRecords
|
|
13
17
|
|
|
14
18
|
|
|
19
|
+
def _safe_delete_file(filename):
|
|
20
|
+
try:
|
|
21
|
+
os.remove(filename)
|
|
22
|
+
except Exception as ex:
|
|
23
|
+
logger().info(
|
|
24
|
+
f"_safe_delete_file - silently_catch_error - {str(ex)}",
|
|
25
|
+
exc_info=True,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
15
29
|
@contextmanager
|
|
16
30
|
def watcher_external(
|
|
17
31
|
records: HTTPRecords,
|
|
@@ -77,12 +91,41 @@ class WatcherSubprocessDirThread(threading.Thread):
|
|
|
77
91
|
threading.Thread.__init__(self)
|
|
78
92
|
|
|
79
93
|
def load_dump(self):
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
self.records.
|
|
94
|
+
# initiators
|
|
95
|
+
for filename in glob.glob(os.path.join(self.directory, "*.httpdbg-initiator")):
|
|
96
|
+
with open(filename, "r") as f:
|
|
97
|
+
data = json.load(f)
|
|
98
|
+
initiator = Initiator.from_json(data)
|
|
99
|
+
self.records.initiators[initiator.id] = initiator
|
|
100
|
+
_safe_delete_file(filename)
|
|
101
|
+
|
|
102
|
+
# group
|
|
103
|
+
for filename in glob.glob(os.path.join(self.directory, "*.httpdbg-group")):
|
|
104
|
+
with open(filename, "r") as f:
|
|
105
|
+
data = json.load(f)
|
|
106
|
+
group = Group.from_json(data)
|
|
107
|
+
self.records.groups[group.id] = group
|
|
108
|
+
_safe_delete_file(filename)
|
|
109
|
+
|
|
110
|
+
# httprecord HTTP1
|
|
111
|
+
for filename in sorted(
|
|
112
|
+
glob.glob(os.path.join(self.directory, "*.httpdbg-http1record"))
|
|
113
|
+
):
|
|
114
|
+
with open(filename, "r") as f:
|
|
115
|
+
data = json.load(f)
|
|
116
|
+
http1record = HTTP1Record.from_json(data)
|
|
117
|
+
self.records.requests[http1record.id] = http1record
|
|
118
|
+
_safe_delete_file(filename)
|
|
119
|
+
|
|
120
|
+
# httprecord HTTP2
|
|
121
|
+
for filename in sorted(
|
|
122
|
+
glob.glob(os.path.join(self.directory, "*.httpdbg-http2record"))
|
|
123
|
+
):
|
|
124
|
+
with open(filename, "r") as f:
|
|
125
|
+
data = json.load(f)
|
|
126
|
+
http2record = HTTP2Record.from_json(data)
|
|
127
|
+
self.records.requests[http2record.id] = http2record
|
|
128
|
+
_safe_delete_file(filename)
|
|
86
129
|
|
|
87
130
|
def run(self):
|
|
88
131
|
while self._running:
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
2
|
import datetime
|
|
3
|
+
import json
|
|
4
|
+
import traceback
|
|
5
|
+
from typing import Any
|
|
3
6
|
from urllib.parse import urlparse
|
|
4
7
|
|
|
5
8
|
from httpdbg.utils import get_new_uuid
|
|
@@ -176,3 +179,62 @@ class HTTPRecord(ABC):
|
|
|
176
179
|
return self.request.protocol
|
|
177
180
|
else:
|
|
178
181
|
return f"{self.request.protocol} -> {self.response.protocol}"
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def payload(self) -> bytes:
|
|
185
|
+
payload: dict[str, Any] = {
|
|
186
|
+
"id": self.id,
|
|
187
|
+
"url": self.url,
|
|
188
|
+
"netloc": self.netloc,
|
|
189
|
+
"urlext": self.urlext,
|
|
190
|
+
"method": self.method,
|
|
191
|
+
"status_code": self.status_code,
|
|
192
|
+
"http100": self.http100,
|
|
193
|
+
"reason": self.reason,
|
|
194
|
+
"request": None,
|
|
195
|
+
"response": None,
|
|
196
|
+
"exception": None,
|
|
197
|
+
"tag": self.tag,
|
|
198
|
+
"initiator_id": self.initiator_id,
|
|
199
|
+
"group_id": self.group_id,
|
|
200
|
+
"in_progress": self.in_progress,
|
|
201
|
+
"is_server": not self.is_client,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
payload["request"] = {
|
|
205
|
+
"headers": [header.to_json() for header in self.request.headers],
|
|
206
|
+
"cookies": [cookie.to_json() for cookie in self.request.cookies],
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if self.request.content:
|
|
210
|
+
payload["request"]["body"] = {
|
|
211
|
+
"path": f"/request/{self.id}/up",
|
|
212
|
+
"filename": "upload",
|
|
213
|
+
}
|
|
214
|
+
payload["request"]["body"].update(self.request.preview)
|
|
215
|
+
|
|
216
|
+
payload["response"] = {
|
|
217
|
+
"headers": [header.to_json() for header in self.response.headers],
|
|
218
|
+
"cookies": [cookie.to_json() for cookie in self.response.cookies],
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if self.response.content:
|
|
222
|
+
payload["response"]["body"] = {
|
|
223
|
+
"path": f"/request/{self.id}/down",
|
|
224
|
+
"filename": "download",
|
|
225
|
+
}
|
|
226
|
+
payload["response"]["body"].update(self.response.preview)
|
|
227
|
+
|
|
228
|
+
if self.exception is not None:
|
|
229
|
+
payload["exception"] = {
|
|
230
|
+
"type": str(type(self.exception)),
|
|
231
|
+
"message": "".join(
|
|
232
|
+
traceback.format_exception(
|
|
233
|
+
type(self.exception),
|
|
234
|
+
self.exception,
|
|
235
|
+
self.exception.__traceback__,
|
|
236
|
+
)
|
|
237
|
+
),
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return json.dumps(payload).encode("utf-8")
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import base64
|
|
1
2
|
import datetime
|
|
2
3
|
|
|
3
4
|
from httpdbg.preview import generate_preview
|
|
@@ -195,3 +196,34 @@ class HTTP1Record(HTTPRecord):
|
|
|
195
196
|
self.request.rawdata += data
|
|
196
197
|
else:
|
|
197
198
|
self.response.rawdata += data
|
|
199
|
+
|
|
200
|
+
def to_json(self) -> dict:
|
|
201
|
+
return {
|
|
202
|
+
"id": self.id,
|
|
203
|
+
"initiator_id": self.initiator_id,
|
|
204
|
+
"group_id": self.group_id,
|
|
205
|
+
"tag": self.tag,
|
|
206
|
+
"tbegin": self.tbegin.isoformat(),
|
|
207
|
+
"is_client": self.is_client,
|
|
208
|
+
"received": base64.b64encode(
|
|
209
|
+
self.response.rawdata if self.is_client else self.request.rawdata
|
|
210
|
+
).decode("ascii"),
|
|
211
|
+
"sent": base64.b64encode(
|
|
212
|
+
self.request.rawdata if self.is_client else self.response.rawdata,
|
|
213
|
+
).decode("ascii"),
|
|
214
|
+
"last_update": self.last_update.isoformat(),
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
@staticmethod
|
|
218
|
+
def from_json(data: dict) -> "HTTP1Record":
|
|
219
|
+
http1record = HTTP1Record(
|
|
220
|
+
data["initiator_id"],
|
|
221
|
+
data["group_id"],
|
|
222
|
+
data["tag"],
|
|
223
|
+
datetime.datetime.fromisoformat(data["tbegin"]),
|
|
224
|
+
data["is_client"],
|
|
225
|
+
)
|
|
226
|
+
http1record.id = data["id"]
|
|
227
|
+
http1record.receive_data(base64.b64decode(data["received"]))
|
|
228
|
+
http1record.send_data(base64.b64decode(data["sent"]))
|
|
229
|
+
return http1record
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import base64
|
|
1
2
|
import datetime
|
|
2
3
|
|
|
3
4
|
from httpdbg.preview import generate_preview
|
|
@@ -111,3 +112,42 @@ class HTTP2Record(HTTPRecord):
|
|
|
111
112
|
self.request.content += data
|
|
112
113
|
else:
|
|
113
114
|
self.response.content += data
|
|
115
|
+
|
|
116
|
+
def to_json(self) -> dict:
|
|
117
|
+
return {
|
|
118
|
+
"id": self.id,
|
|
119
|
+
"initiator_id": self.initiator_id,
|
|
120
|
+
"group_id": self.group_id,
|
|
121
|
+
"tag": self.tag,
|
|
122
|
+
"tbegin": self.tbegin.isoformat(),
|
|
123
|
+
"is_client": self.is_client,
|
|
124
|
+
"request": {
|
|
125
|
+
"headers": [header.to_json() for header in self.request.headers],
|
|
126
|
+
"content": base64.b64encode(self.request.content).decode("ascii"),
|
|
127
|
+
},
|
|
128
|
+
"response": {
|
|
129
|
+
"headers": [header.to_json() for header in self.response.headers],
|
|
130
|
+
"content": base64.b64encode(self.response.content).decode("ascii"),
|
|
131
|
+
},
|
|
132
|
+
"last_update": self.last_update.isoformat(),
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@staticmethod
|
|
136
|
+
def from_json(data: dict) -> "HTTP2Record":
|
|
137
|
+
http2record = HTTP2Record(
|
|
138
|
+
data["initiator_id"],
|
|
139
|
+
data["group_id"],
|
|
140
|
+
data["tag"],
|
|
141
|
+
datetime.datetime.fromisoformat(data["tbegin"]),
|
|
142
|
+
data["is_client"],
|
|
143
|
+
)
|
|
144
|
+
http2record.id = data["id"]
|
|
145
|
+
http2record.request._headers = [
|
|
146
|
+
HTTPDBGHeader.from_json(header) for header in data["request"]["headers"]
|
|
147
|
+
]
|
|
148
|
+
http2record.request.content = base64.b64decode(data["request"]["content"])
|
|
149
|
+
http2record.response._headers = [
|
|
150
|
+
HTTPDBGHeader.from_json(header) for header in data["response"]["headers"]
|
|
151
|
+
]
|
|
152
|
+
http2record.response.content = base64.b64decode(data["response"]["content"])
|
|
153
|
+
return http2record
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import atexit
|
|
2
|
+
import json
|
|
2
3
|
import os
|
|
3
|
-
import pickle
|
|
4
4
|
import sys
|
|
5
5
|
import time
|
|
6
6
|
import threading
|
|
@@ -9,12 +9,22 @@ import uuid
|
|
|
9
9
|
from httpdbg import HTTPRecords
|
|
10
10
|
from httpdbg.env import HTTPDBG_MULTIPROCESS_DIR
|
|
11
11
|
from httpdbg.hooks.all import httprecord
|
|
12
|
+
from httpdbg.hooks.record import HTTPRecord
|
|
13
|
+
from httpdbg.hooks.recordhttp1 import HTTP1Record
|
|
14
|
+
from httpdbg.hooks.recordhttp2 import HTTP2Record
|
|
12
15
|
from httpdbg.log import logger
|
|
13
16
|
|
|
14
17
|
HTTPDBG_INITIATORS = [] # type: ignore
|
|
15
18
|
HTTPDBG_RECORD_SERVER = False
|
|
16
19
|
|
|
17
20
|
|
|
21
|
+
def _safe_write_json_file(filename, content):
|
|
22
|
+
with open(f"{filename}.tmp", "wb") as f:
|
|
23
|
+
f.write(json.dumps(content).encode("utf-8"))
|
|
24
|
+
|
|
25
|
+
os.replace(f"{filename}.tmp", filename)
|
|
26
|
+
|
|
27
|
+
|
|
18
28
|
class HttpdbgRecorder:
|
|
19
29
|
"""Records the HTTP requests in subprocesses and saves them to disk."""
|
|
20
30
|
|
|
@@ -23,8 +33,7 @@ class HttpdbgRecorder:
|
|
|
23
33
|
self.fname: str = os.path.join(
|
|
24
34
|
os.environ[HTTPDBG_MULTIPROCESS_DIR], str(uuid.uuid1())
|
|
25
35
|
)
|
|
26
|
-
self.
|
|
27
|
-
self.records: HTTPRecords | None = None
|
|
36
|
+
self.serialized: list[str] = []
|
|
28
37
|
|
|
29
38
|
def _save_to_disk_loop(self):
|
|
30
39
|
while self._running:
|
|
@@ -44,19 +53,39 @@ class HttpdbgRecorder:
|
|
|
44
53
|
time.sleep(1)
|
|
45
54
|
|
|
46
55
|
def save_to_disk(self):
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
f"{self.fname}.
|
|
52
|
-
|
|
56
|
+
# initiators
|
|
57
|
+
for initiator in self.records.initiators.values():
|
|
58
|
+
if initiator.id not in self.serialized:
|
|
59
|
+
self.serialized.append(initiator.id)
|
|
60
|
+
filename = f"{self.fname}.{initiator.id}.httpdbg-initiator"
|
|
61
|
+
_safe_write_json_file(filename, initiator.to_json())
|
|
62
|
+
|
|
63
|
+
# groups
|
|
64
|
+
for group in self.records.groups.values():
|
|
65
|
+
if group.id not in self.serialized:
|
|
66
|
+
self.serialized.append(group.id)
|
|
67
|
+
filename = f"{self.fname}.{group.id}.httpdbg-group"
|
|
68
|
+
_safe_write_json_file(filename, group.to_json())
|
|
69
|
+
|
|
70
|
+
# requests
|
|
71
|
+
for request in self.records.requests.values():
|
|
72
|
+
if isinstance(request, HTTPRecord):
|
|
73
|
+
marker = f"{request.id}-{request.last_update.isoformat()}"
|
|
74
|
+
if marker not in self.serialized:
|
|
75
|
+
self.serialized.append(marker)
|
|
76
|
+
if isinstance(request, HTTP1Record):
|
|
77
|
+
filename = f"{self.fname}.{marker}.httpdbg-http1record"
|
|
78
|
+
_safe_write_json_file(filename, request.to_json())
|
|
79
|
+
elif isinstance(request, HTTP2Record):
|
|
80
|
+
filename = f"{self.fname}.{marker}.httpdbg-http2record"
|
|
81
|
+
_safe_write_json_file(filename, request.to_json())
|
|
53
82
|
|
|
54
83
|
def start(self):
|
|
55
84
|
"""Start recording."""
|
|
56
85
|
self.context = httprecord(
|
|
57
86
|
initiators=HTTPDBG_INITIATORS, server=HTTPDBG_RECORD_SERVER
|
|
58
87
|
)
|
|
59
|
-
self.records = self.context.__enter__()
|
|
88
|
+
self.records: HTTPRecords = self.context.__enter__()
|
|
60
89
|
self._running = True
|
|
61
90
|
self._thread = threading.Thread(target=self._save_to_disk_loop, daemon=True)
|
|
62
91
|
self._thread.start()
|
|
@@ -59,6 +59,17 @@ class Initiator:
|
|
|
59
59
|
}
|
|
60
60
|
return json
|
|
61
61
|
|
|
62
|
+
@staticmethod
|
|
63
|
+
def from_json(data: dict) -> "Initiator":
|
|
64
|
+
initiator = Initiator(
|
|
65
|
+
data["label"],
|
|
66
|
+
data["short_stack"],
|
|
67
|
+
data["stack"].split("\n"),
|
|
68
|
+
)
|
|
69
|
+
initiator.id = data["id"]
|
|
70
|
+
initiator.tbegin = datetime.datetime.fromisoformat(data["tbegin"])
|
|
71
|
+
return initiator
|
|
72
|
+
|
|
62
73
|
|
|
63
74
|
class Group:
|
|
64
75
|
def __init__(self, label: str, full_label: str, updatable: bool):
|
|
@@ -76,6 +87,17 @@ class Group:
|
|
|
76
87
|
"tbegin": self.tbegin.isoformat(),
|
|
77
88
|
}
|
|
78
89
|
|
|
90
|
+
@staticmethod
|
|
91
|
+
def from_json(data: dict) -> "Group":
|
|
92
|
+
group = Group(
|
|
93
|
+
data["label"],
|
|
94
|
+
data["full_label"],
|
|
95
|
+
False,
|
|
96
|
+
)
|
|
97
|
+
group.id = data["id"]
|
|
98
|
+
group.tbegin = datetime.datetime.fromisoformat(data["tbegin"])
|
|
99
|
+
return group
|
|
100
|
+
|
|
79
101
|
|
|
80
102
|
def compatible_path(path: str) -> str:
|
|
81
103
|
p = path
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import datetime
|
|
2
|
+
import json
|
|
2
3
|
import sys
|
|
3
4
|
|
|
4
5
|
from httpdbg.hooks.record import HTTPRecord
|
|
@@ -89,3 +90,44 @@ class HTTPRecords:
|
|
|
89
90
|
print(f"+ {request.url}")
|
|
90
91
|
print(f" - initiator: {self.initiators[request.initiator_id].label}")
|
|
91
92
|
print(f" - group: {self.groups[request.group_id].label}")
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def payload(self) -> bytes:
|
|
96
|
+
payload: dict[str, dict[str, str | dict]] = {
|
|
97
|
+
"session": {
|
|
98
|
+
"id": self.session.id,
|
|
99
|
+
"command_line": self.session.command_line,
|
|
100
|
+
"tbegin": self.session.tbegin.isoformat(),
|
|
101
|
+
},
|
|
102
|
+
"requests": {},
|
|
103
|
+
"initiators": {},
|
|
104
|
+
"groups": {},
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for id, req in self.requests.items():
|
|
108
|
+
payload["requests"][id] = {
|
|
109
|
+
"id": req.id,
|
|
110
|
+
"url": req.url,
|
|
111
|
+
"netloc": req.netloc,
|
|
112
|
+
"urlext": req.urlext,
|
|
113
|
+
"status_code": req.status_code,
|
|
114
|
+
"http100": req.http100,
|
|
115
|
+
"protocol": req.protocol,
|
|
116
|
+
"reason": req.reason,
|
|
117
|
+
"verb": req.method,
|
|
118
|
+
"tag": req.tag,
|
|
119
|
+
"initiator_id": req.initiator_id,
|
|
120
|
+
"group_id": req.group_id,
|
|
121
|
+
"in_progress": req.in_progress,
|
|
122
|
+
"is_server": not req.is_client,
|
|
123
|
+
"tbegin": req.tbegin.isoformat(),
|
|
124
|
+
"last_update": req.last_update.isoformat(),
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for id, initiator in self.initiators.items():
|
|
128
|
+
payload["initiators"][id] = initiator.to_json(full=True)
|
|
129
|
+
|
|
130
|
+
for id, group in self.groups.items():
|
|
131
|
+
payload["groups"][id] = group.to_json()
|
|
132
|
+
|
|
133
|
+
return json.dumps(payload).encode("utf-8")
|
|
@@ -66,6 +66,10 @@ class HTTPDBGHeader(object):
|
|
|
66
66
|
def to_json(self) -> dict:
|
|
67
67
|
return {"name": self.name, "value": self.value}
|
|
68
68
|
|
|
69
|
+
@staticmethod
|
|
70
|
+
def from_json(data) -> "HTTPDBGHeader":
|
|
71
|
+
return HTTPDBGHeader(data["name"], data["value"])
|
|
72
|
+
|
|
69
73
|
def __eq__(self, other) -> bool:
|
|
70
74
|
if type(other) is HTTPDBGHeader:
|
|
71
75
|
return (self.name == other.name) and (self.value == other.value)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from collections.abc import Callable
|
|
2
2
|
from contextlib import contextmanager
|
|
3
3
|
from http.server import BaseHTTPRequestHandler
|
|
4
|
-
import json
|
|
5
4
|
from pathlib import Path
|
|
6
5
|
import re
|
|
7
6
|
from urllib.parse import ParseResult
|
|
@@ -9,7 +8,6 @@ from urllib.parse import parse_qs
|
|
|
9
8
|
from urllib.parse import urlparse
|
|
10
9
|
|
|
11
10
|
from httpdbg.log import logger
|
|
12
|
-
from httpdbg.webapp.api import RequestListPayload, RequestPayload
|
|
13
11
|
from httpdbg.records import HTTPRecords
|
|
14
12
|
|
|
15
13
|
|
|
@@ -97,9 +95,7 @@ class HttpbgHTTPRequestHandler(BaseHTTPRequestHandler):
|
|
|
97
95
|
self.send_header("Content-type", "application/json")
|
|
98
96
|
self.send_header_no_cache()
|
|
99
97
|
self.end_headers()
|
|
100
|
-
self.wfile.write(
|
|
101
|
-
json.dumps(self.records, cls=RequestListPayload).encode("utf-8")
|
|
102
|
-
)
|
|
98
|
+
self.wfile.write(self.records.payload)
|
|
103
99
|
|
|
104
100
|
return True
|
|
105
101
|
|
|
@@ -118,11 +114,7 @@ class HttpbgHTTPRequestHandler(BaseHTTPRequestHandler):
|
|
|
118
114
|
self.send_header("Content-type", "application/json")
|
|
119
115
|
self.send_header_no_cache()
|
|
120
116
|
self.end_headers()
|
|
121
|
-
self.wfile.write(
|
|
122
|
-
json.dumps(self.records.requests[req_id], cls=RequestPayload).encode(
|
|
123
|
-
"utf-8"
|
|
124
|
-
)
|
|
125
|
-
)
|
|
117
|
+
self.wfile.write(self.records.requests[req_id].payload)
|
|
126
118
|
|
|
127
119
|
return True
|
|
128
120
|
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
from json import JSONEncoder
|
|
2
|
-
import traceback
|
|
3
|
-
from typing import Any
|
|
4
|
-
|
|
5
|
-
from httpdbg.records import HTTPRecords
|
|
6
|
-
from httpdbg.records import HTTPRecord
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class RequestPayload(JSONEncoder):
|
|
10
|
-
def default(self, req: HTTPRecord) -> dict[str, Any]:
|
|
11
|
-
payload: dict[str, Any] = {
|
|
12
|
-
"id": req.id,
|
|
13
|
-
"url": req.url,
|
|
14
|
-
"netloc": req.netloc,
|
|
15
|
-
"urlext": req.urlext,
|
|
16
|
-
"method": req.method,
|
|
17
|
-
"status_code": req.status_code,
|
|
18
|
-
"http100": req.http100,
|
|
19
|
-
"reason": req.reason,
|
|
20
|
-
"request": None,
|
|
21
|
-
"response": None,
|
|
22
|
-
"exception": None,
|
|
23
|
-
"tag": req.tag,
|
|
24
|
-
"initiator_id": req.initiator_id,
|
|
25
|
-
"group_id": req.group_id,
|
|
26
|
-
"in_progress": req.in_progress,
|
|
27
|
-
"is_server": not req.is_client,
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
payload["request"] = {
|
|
31
|
-
"headers": [header.to_json() for header in req.request.headers],
|
|
32
|
-
"cookies": [cookie.to_json() for cookie in req.request.cookies],
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if req.request.content:
|
|
36
|
-
payload["request"]["body"] = {
|
|
37
|
-
"path": f"/request/{req.id}/up",
|
|
38
|
-
"filename": "upload",
|
|
39
|
-
}
|
|
40
|
-
payload["request"]["body"].update(req.request.preview)
|
|
41
|
-
|
|
42
|
-
payload["response"] = {
|
|
43
|
-
"headers": [header.to_json() for header in req.response.headers],
|
|
44
|
-
"cookies": [cookie.to_json() for cookie in req.response.cookies],
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if req.response.content:
|
|
48
|
-
payload["response"]["body"] = {
|
|
49
|
-
"path": f"/request/{req.id}/down",
|
|
50
|
-
"filename": "download",
|
|
51
|
-
}
|
|
52
|
-
payload["response"]["body"].update(req.response.preview)
|
|
53
|
-
|
|
54
|
-
if req.exception is not None:
|
|
55
|
-
payload["exception"] = {
|
|
56
|
-
"type": str(type(req.exception)),
|
|
57
|
-
"message": "".join(
|
|
58
|
-
traceback.format_exception(
|
|
59
|
-
type(req.exception), req.exception, req.exception.__traceback__
|
|
60
|
-
)
|
|
61
|
-
),
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return payload
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
class RequestListPayload(JSONEncoder):
|
|
68
|
-
def default(self, records: HTTPRecords):
|
|
69
|
-
assert isinstance(
|
|
70
|
-
records, HTTPRecords
|
|
71
|
-
), "This encoder works only for HTTPRecords object."
|
|
72
|
-
|
|
73
|
-
payload: dict[str, dict[str, str | dict]] = {
|
|
74
|
-
"session": {
|
|
75
|
-
"id": records.session.id,
|
|
76
|
-
"command_line": records.session.command_line,
|
|
77
|
-
"tbegin": records.session.tbegin.isoformat(),
|
|
78
|
-
},
|
|
79
|
-
"requests": {},
|
|
80
|
-
"initiators": {},
|
|
81
|
-
"groups": {},
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
for id, req in records.requests.items():
|
|
85
|
-
payload["requests"][id] = {
|
|
86
|
-
"id": req.id,
|
|
87
|
-
"url": req.url,
|
|
88
|
-
"netloc": req.netloc,
|
|
89
|
-
"urlext": req.urlext,
|
|
90
|
-
"status_code": req.status_code,
|
|
91
|
-
"http100": req.http100,
|
|
92
|
-
"protocol": req.protocol,
|
|
93
|
-
"reason": req.reason,
|
|
94
|
-
"verb": req.method,
|
|
95
|
-
"tag": req.tag,
|
|
96
|
-
"initiator_id": req.initiator_id,
|
|
97
|
-
"group_id": req.group_id,
|
|
98
|
-
"in_progress": req.in_progress,
|
|
99
|
-
"is_server": not req.is_client,
|
|
100
|
-
"tbegin": req.tbegin.isoformat(),
|
|
101
|
-
"last_update": req.last_update.isoformat(),
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
for id, initiator in records.initiators.items():
|
|
105
|
-
payload["initiators"][id] = initiator.to_json(full=True)
|
|
106
|
-
|
|
107
|
-
for id, group in records.groups.items():
|
|
108
|
-
payload["groups"][id] = group.to_json()
|
|
109
|
-
|
|
110
|
-
return payload
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/chevron-direction-top-outline-icon.svg
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/question-mark-round-line-icon.svg
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|