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.
Files changed (98) hide show
  1. {httpdbg-2.3.0 → httpdbg-2.4.0}/PKG-INFO +1 -1
  2. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/__init__.py +1 -1
  3. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/export.py +2 -5
  4. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/external.py +50 -7
  5. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/record.py +62 -0
  6. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/recordhttp1.py +32 -0
  7. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/recordhttp2.py +40 -0
  8. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/template_sitecustomize.py +39 -10
  9. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/initiator.py +22 -0
  10. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/records.py +42 -0
  11. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/utils.py +4 -0
  12. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/app.py +2 -10
  13. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg.egg-info/PKG-INFO +1 -1
  14. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg.egg-info/SOURCES.txt +0 -1
  15. httpdbg-2.3.0/httpdbg/webapp/api.py +0 -110
  16. {httpdbg-2.3.0 → httpdbg-2.4.0}/LICENSE +0 -0
  17. {httpdbg-2.3.0 → httpdbg-2.4.0}/README.md +0 -0
  18. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/__main__.py +0 -0
  19. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/args.py +0 -0
  20. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/env.py +0 -0
  21. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/exception.py +0 -0
  22. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/__init__.py +0 -0
  23. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/aiohttp.py +0 -0
  24. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/all.py +0 -0
  25. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/fastapi.py +0 -0
  26. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/flask.py +0 -0
  27. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/generic.py +0 -0
  28. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/h2.py +0 -0
  29. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/http.py +0 -0
  30. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/httpx.py +0 -0
  31. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/pytest.py +0 -0
  32. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/requests.py +0 -0
  33. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/socket.py +0 -0
  34. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/starlette.py +0 -0
  35. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/unittest.py +0 -0
  36. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/urllib3.py +0 -0
  37. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/utils.py +0 -0
  38. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/hooks/uvicorn.py +0 -0
  39. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/http_status_code.py +0 -0
  40. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/log.py +0 -0
  41. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/mode_console.py +0 -0
  42. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/mode_module.py +0 -0
  43. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/mode_script.py +0 -0
  44. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/preview.py +0 -0
  45. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/server.py +0 -0
  46. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/__init__.py +0 -0
  47. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/api.js +0 -0
  48. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/column.css +0 -0
  49. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/configuration.css +0 -0
  50. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/configuration.js +0 -0
  51. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/detail.css +0 -0
  52. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/favicon.ico +0 -0
  53. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/filter.js +0 -0
  54. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/chevron-direction-top-outline-icon.svg +0 -0
  55. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/download-install-line-icon.svg +0 -0
  56. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/engine-gears-icon.svg +0 -0
  57. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/eye-blind-icon.svg +0 -0
  58. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/eye-icon.svg +0 -0
  59. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/files-papers-outline-icon.svg +0 -0
  60. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/filter-outline-icon.svg +0 -0
  61. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/lock-line-icon.svg +0 -0
  62. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/lock-open-line-icon.svg +0 -0
  63. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/math-multiplication-icon.svg +0 -0
  64. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/push-pin-green-icon.svg +0 -0
  65. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/question-mark-round-line-icon.svg +0 -0
  66. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/recycle-bin-line-icon.svg +0 -0
  67. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/wait-sandclock-icon.svg +0 -0
  68. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/index.htm +0 -0
  69. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/main.js +0 -0
  70. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/mustache.min.js +0 -0
  71. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/render.js +0 -0
  72. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/style.css +0 -0
  73. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg/webapp/static/table.css +0 -0
  74. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg.egg-info/dependency_links.txt +0 -0
  75. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg.egg-info/entry_points.txt +0 -0
  76. {httpdbg-2.3.0 → httpdbg-2.4.0}/httpdbg.egg-info/top_level.txt +0 -0
  77. {httpdbg-2.3.0 → httpdbg-2.4.0}/pyproject.toml +0 -0
  78. {httpdbg-2.3.0 → httpdbg-2.4.0}/setup.cfg +0 -0
  79. {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_api.py +0 -0
  80. {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_cli.py +0 -0
  81. {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_context_manager.py +0 -0
  82. {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_hook_aiohttp.py +0 -0
  83. {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_hook_h2.py +0 -0
  84. {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_hook_httpx.py +0 -0
  85. {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_hook_pytest.py +0 -0
  86. {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_hook_requests.py +0 -0
  87. {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_hook_unittest.py +0 -0
  88. {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_hook_urllib3.py +0 -0
  89. {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_initiator.py +0 -0
  90. {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_mode_console.py +0 -0
  91. {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_mode_module_pytest.py +0 -0
  92. {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_mode_script.py +0 -0
  93. {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_multiprocess.py +0 -0
  94. {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_preview.py +0 -0
  95. {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_pyhttpdbg_server.py +0 -0
  96. {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_server.py +0 -0
  97. {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_server_hook_fastapi.py +0 -0
  98. {httpdbg-2.3.0 → httpdbg-2.4.0}/tests/test_server_hook_flask.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: httpdbg
3
- Version: 2.3.0
3
+ Version: 2.4.0
4
4
  Summary: A very simple tool to debug HTTP(S) client and server requests.
5
5
  Author-email: cle-b <cle@tictac.pm>
6
6
  License-Expression: Apache-2.0
@@ -1,4 +1,4 @@
1
- __version__ = "2.3.0"
1
+ __version__ = "2.4.0"
2
2
 
3
3
  __all__ = ["export_html", "httprecord", "HTTPRecords"]
4
4
 
@@ -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 = json.dumps(
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(json.dumps(record, cls=RequestPayload))
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
- for dump in glob.glob(os.path.join(self.directory, "*.httpdbgrecords")):
81
- with open(dump, "rb") as dumpfile:
82
- newrecords: HTTPRecords = pickle.load(dumpfile)
83
- self.records.requests.update(newrecords.requests)
84
- self.records.initiators.update(newrecords.initiators)
85
- self.records.groups.update(newrecords.groups)
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.context = None
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
- if self.records and len(self.records.requests) > 0:
48
- with open(f"{self.fname}.httpdbgrecords.tmp", "wb") as f:
49
- pickle.dump(self.records, f)
50
- os.replace(
51
- f"{self.fname}.httpdbgrecords.tmp", f"{self.fname}.httpdbgrecords"
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: httpdbg
3
- Version: 2.3.0
3
+ Version: 2.4.0
4
4
  Summary: A very simple tool to debug HTTP(S) client and server requests.
5
5
  Author-email: cle-b <cle@tictac.pm>
6
6
  License-Expression: Apache-2.0
@@ -46,7 +46,6 @@ httpdbg/hooks/urllib3.py
46
46
  httpdbg/hooks/utils.py
47
47
  httpdbg/hooks/uvicorn.py
48
48
  httpdbg/webapp/__init__.py
49
- httpdbg/webapp/api.py
50
49
  httpdbg/webapp/app.py
51
50
  httpdbg/webapp/static/api.js
52
51
  httpdbg/webapp/static/column.css
@@ -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