httpdbg 2.2.1__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.2.1 → httpdbg-2.4.0}/PKG-INFO +3 -3
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/__init__.py +1 -1
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/__main__.py +1 -2
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/export.py +2 -5
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/aiohttp.py +1 -1
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/all.py +2 -3
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/external.py +52 -10
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/fastapi.py +2 -3
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/flask.py +1 -1
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/generic.py +2 -3
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/h2.py +2 -3
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/http.py +1 -2
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/httpx.py +1 -1
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/pytest.py +1 -1
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/record.py +66 -5
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/recordhttp1.py +32 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/recordhttp2.py +40 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/requests.py +1 -1
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/socket.py +5 -6
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/starlette.py +1 -1
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/template_sitecustomize.py +39 -11
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/unittest.py +1 -1
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/urllib3.py +1 -1
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/uvicorn.py +1 -1
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/initiator.py +24 -3
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/log.py +1 -2
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/mode_console.py +3 -8
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/preview.py +2 -5
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/records.py +45 -4
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/server.py +1 -1
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/utils.py +4 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/app.py +2 -10
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg.egg-info/PKG-INFO +3 -3
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg.egg-info/SOURCES.txt +0 -1
- {httpdbg-2.2.1 → httpdbg-2.4.0}/pyproject.toml +3 -3
- httpdbg-2.2.1/httpdbg/webapp/api.py +0 -111
- {httpdbg-2.2.1 → httpdbg-2.4.0}/LICENSE +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/README.md +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/args.py +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/env.py +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/exception.py +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/__init__.py +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/hooks/utils.py +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/http_status_code.py +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/mode_module.py +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/mode_script.py +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/__init__.py +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/api.js +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/column.css +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/configuration.css +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/configuration.js +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/detail.css +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/favicon.ico +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/filter.js +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/chevron-direction-top-outline-icon.svg +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/download-install-line-icon.svg +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/engine-gears-icon.svg +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/eye-blind-icon.svg +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/eye-icon.svg +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/files-papers-outline-icon.svg +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/filter-outline-icon.svg +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/lock-line-icon.svg +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/lock-open-line-icon.svg +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/math-multiplication-icon.svg +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/push-pin-green-icon.svg +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/question-mark-round-line-icon.svg +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/recycle-bin-line-icon.svg +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/icons/wait-sandclock-icon.svg +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/index.htm +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/main.js +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/mustache.min.js +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/render.js +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/style.css +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg/webapp/static/table.css +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg.egg-info/dependency_links.txt +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg.egg-info/entry_points.txt +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/httpdbg.egg-info/top_level.txt +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/setup.cfg +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/tests/test_api.py +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/tests/test_cli.py +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/tests/test_context_manager.py +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/tests/test_hook_aiohttp.py +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/tests/test_hook_h2.py +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/tests/test_hook_httpx.py +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/tests/test_hook_pytest.py +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/tests/test_hook_requests.py +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/tests/test_hook_unittest.py +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/tests/test_hook_urllib3.py +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/tests/test_initiator.py +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/tests/test_mode_console.py +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/tests/test_mode_module_pytest.py +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/tests/test_mode_script.py +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/tests/test_multiprocess.py +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/tests/test_preview.py +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/tests/test_pyhttpdbg_server.py +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/tests/test_server.py +0 -0
- {httpdbg-2.2.1 → httpdbg-2.4.0}/tests/test_server_hook_fastapi.py +0 -0
- {httpdbg-2.2.1 → 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
|
+
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
|
|
@@ -10,16 +10,16 @@ Classifier: Development Status :: 5 - Production/Stable
|
|
|
10
10
|
Classifier: Intended Audience :: Developers
|
|
11
11
|
Classifier: Operating System :: OS Independent
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.10
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.12
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.13
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
19
19
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
20
20
|
Classifier: Topic :: Software Development :: Testing
|
|
21
21
|
Classifier: Topic :: Utilities
|
|
22
|
-
Requires-Python: >=3.
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
23
|
Description-Content-Type: text/markdown
|
|
24
24
|
License-File: LICENSE
|
|
25
25
|
Dynamic: license-file
|
|
@@ -2,7 +2,6 @@ import argparse
|
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
import sys
|
|
4
4
|
import time
|
|
5
|
-
from typing import Union
|
|
6
5
|
|
|
7
6
|
from httpdbg import __version__
|
|
8
7
|
from httpdbg.args import read_args
|
|
@@ -32,7 +31,7 @@ def pyhttpdbg(
|
|
|
32
31
|
|
|
33
32
|
def run_recorder(
|
|
34
33
|
records: HTTPRecords,
|
|
35
|
-
initiators:
|
|
34
|
+
initiators: list[str] | None,
|
|
36
35
|
record_server: bool,
|
|
37
36
|
test_mode: bool,
|
|
38
37
|
):
|
|
@@ -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,6 +1,5 @@
|
|
|
1
|
+
from collections.abc import Generator
|
|
1
2
|
from contextlib import contextmanager
|
|
2
|
-
from typing import Generator
|
|
3
|
-
from typing import Union
|
|
4
3
|
|
|
5
4
|
from httpdbg.hooks.aiohttp import hook_aiohttp
|
|
6
5
|
from httpdbg.hooks.external import watcher_external
|
|
@@ -24,7 +23,7 @@ from httpdbg.records import HTTPRecords
|
|
|
24
23
|
@contextmanager
|
|
25
24
|
def httprecord(
|
|
26
25
|
records: HTTPRecords = None,
|
|
27
|
-
initiators:
|
|
26
|
+
initiators: list[str] | None = None,
|
|
28
27
|
client: bool = True,
|
|
29
28
|
server: bool = False,
|
|
30
29
|
ignore: tuple[tuple[str, int], ...] = (),
|
|
@@ -1,22 +1,35 @@
|
|
|
1
|
+
from collections.abc import Generator
|
|
1
2
|
from contextlib import contextmanager
|
|
2
|
-
|
|
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
|
-
from typing import Union
|
|
10
9
|
|
|
11
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
|
|
12
15
|
from httpdbg.log import logger
|
|
13
16
|
from httpdbg.records import HTTPRecords
|
|
14
17
|
|
|
15
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
|
+
|
|
16
29
|
@contextmanager
|
|
17
30
|
def watcher_external(
|
|
18
31
|
records: HTTPRecords,
|
|
19
|
-
initiators:
|
|
32
|
+
initiators: list[str] | None = None,
|
|
20
33
|
server: bool = False,
|
|
21
34
|
) -> Generator[HTTPRecords, None, None]:
|
|
22
35
|
if HTTPDBG_MULTIPROCESS_DIR not in os.environ:
|
|
@@ -78,12 +91,41 @@ class WatcherSubprocessDirThread(threading.Thread):
|
|
|
78
91
|
threading.Thread.__init__(self)
|
|
79
92
|
|
|
80
93
|
def load_dump(self):
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
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)
|
|
87
129
|
|
|
88
130
|
def run(self):
|
|
89
131
|
while self._running:
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
from collections.abc import Callable
|
|
2
|
+
from collections.abc import Generator
|
|
2
3
|
from contextlib import contextmanager
|
|
4
|
+
import datetime
|
|
3
5
|
import functools
|
|
4
6
|
from functools import wraps
|
|
5
|
-
from typing import Generator
|
|
6
|
-
|
|
7
|
-
import datetime
|
|
8
7
|
|
|
9
8
|
from httpdbg.hooks.utils import decorate
|
|
10
9
|
from httpdbg.hooks.utils import getcallargs
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import builtins
|
|
2
2
|
from collections.abc import Callable
|
|
3
|
+
from collections.abc import Generator
|
|
3
4
|
from contextlib import contextmanager
|
|
4
5
|
import functools
|
|
5
6
|
import importlib
|
|
6
7
|
import inspect
|
|
7
8
|
from types import ModuleType
|
|
8
9
|
from typing import Any
|
|
9
|
-
from typing import Generator
|
|
10
|
-
from typing import Union
|
|
11
10
|
|
|
12
11
|
from httpdbg.hooks.utils import decorate
|
|
13
12
|
from httpdbg.hooks.utils import undecorate
|
|
@@ -42,7 +41,7 @@ def set_hook_for_generic(records: HTTPRecords, method: Any):
|
|
|
42
41
|
|
|
43
42
|
@contextmanager
|
|
44
43
|
def hook_generic(
|
|
45
|
-
records: HTTPRecords, initiators:
|
|
44
|
+
records: HTTPRecords, initiators: list[str] | None = None
|
|
46
45
|
) -> Generator[None, None, None]:
|
|
47
46
|
if initiators:
|
|
48
47
|
# we add a hook for a generic initiator only if the module is imported
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
from collections.abc import Callable
|
|
2
|
+
from collections.abc import Generator
|
|
2
3
|
from contextlib import contextmanager
|
|
3
4
|
import random
|
|
4
|
-
from typing import Generator
|
|
5
|
-
from typing import Union
|
|
6
5
|
|
|
7
6
|
from httpdbg.hooks.recordhttp2 import HTTP2Record
|
|
8
7
|
from httpdbg.hooks.utils import decorate
|
|
@@ -67,7 +66,7 @@ class TracerHTTP2:
|
|
|
67
66
|
stream_id: int,
|
|
68
67
|
headers: list[tuple[bytes, bytes]],
|
|
69
68
|
is_client: bool = True,
|
|
70
|
-
) ->
|
|
69
|
+
) -> HTTP2Record | None:
|
|
71
70
|
record = None
|
|
72
71
|
if not is_client:
|
|
73
72
|
if f"{socket_id}-{stream_id}" in self.sockets:
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
2
|
import datetime
|
|
3
|
-
|
|
3
|
+
import json
|
|
4
|
+
import traceback
|
|
5
|
+
from typing import Any
|
|
4
6
|
from urllib.parse import urlparse
|
|
5
7
|
|
|
6
8
|
from httpdbg.utils import get_new_uuid
|
|
@@ -98,12 +100,12 @@ class HTTPRecord(ABC):
|
|
|
98
100
|
) -> None:
|
|
99
101
|
self.id = get_new_uuid()
|
|
100
102
|
self.address: tuple[str, int] = ("", 0)
|
|
101
|
-
self._url:
|
|
103
|
+
self._url: str | None = None
|
|
102
104
|
self.initiator_id: str = initiator_id
|
|
103
|
-
self.exception:
|
|
104
|
-
self.ssl:
|
|
105
|
+
self.exception: Exception | None = None
|
|
106
|
+
self.ssl: bool | None = None
|
|
105
107
|
self.tbegin: datetime.datetime = datetime.datetime.now(datetime.timezone.utc)
|
|
106
|
-
self.tag:
|
|
108
|
+
self.tag: str | None = tag
|
|
107
109
|
self.group_id: str = group_id
|
|
108
110
|
self.is_client: bool = is_client
|
|
109
111
|
if tbegin:
|
|
@@ -177,3 +179,62 @@ class HTTPRecord(ABC):
|
|
|
177
179
|
return self.request.protocol
|
|
178
180
|
else:
|
|
179
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,14 +1,13 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import asyncio.proactor_events
|
|
3
3
|
from collections.abc import Callable
|
|
4
|
+
from collections.abc import Generator
|
|
4
5
|
from contextlib import contextmanager
|
|
5
6
|
import datetime
|
|
6
7
|
import platform
|
|
7
8
|
import socket
|
|
8
9
|
import ssl
|
|
9
10
|
import sys
|
|
10
|
-
from typing import Generator
|
|
11
|
-
from typing import Union
|
|
12
11
|
|
|
13
12
|
from httpdbg.initiator import httpdbg_initiator
|
|
14
13
|
from httpdbg.log import logger
|
|
@@ -28,7 +27,7 @@ class SocketRawData(object):
|
|
|
28
27
|
self.address: tuple[str, int] = address
|
|
29
28
|
self.ssl: bool = ssl
|
|
30
29
|
self._rawdata: bytes = bytes()
|
|
31
|
-
self.record:
|
|
30
|
+
self.record: HTTP1Record | None = None
|
|
32
31
|
self.tbegin: datetime.datetime = datetime.datetime.now(datetime.timezone.utc)
|
|
33
32
|
|
|
34
33
|
@property
|
|
@@ -42,7 +41,7 @@ class SocketRawData(object):
|
|
|
42
41
|
)
|
|
43
42
|
self._rawdata = value
|
|
44
43
|
|
|
45
|
-
def http_detected(self) ->
|
|
44
|
+
def http_detected(self) -> bool | None:
|
|
46
45
|
end_of_first_line = self.rawdata[:2048].find(b"\r\n")
|
|
47
46
|
if end_of_first_line == -1:
|
|
48
47
|
if len(self.rawdata) > 2048:
|
|
@@ -66,14 +65,14 @@ class TracerHTTP1:
|
|
|
66
65
|
self,
|
|
67
66
|
ignore: tuple[tuple[str, int], ...] = (),
|
|
68
67
|
):
|
|
69
|
-
self.sockets: dict[int,
|
|
68
|
+
self.sockets: dict[int, SocketRawData | None] = (
|
|
70
69
|
{}
|
|
71
70
|
) # if None, this is not a HTTP/1 request
|
|
72
71
|
self.ignore: tuple[tuple[str, int], ...] = ignore
|
|
73
72
|
|
|
74
73
|
def get_socket_data(
|
|
75
74
|
self, obj, extra_sock=None, force_new=False, request=None, is_uvicorn=False
|
|
76
|
-
) ->
|
|
75
|
+
) -> SocketRawData | None:
|
|
77
76
|
"""Record a new SocketRawData (or get an existing one) and return it."""
|
|
78
77
|
socketdata = None
|
|
79
78
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from collections.abc import Callable
|
|
2
|
+
from collections.abc import Generator
|
|
2
3
|
from contextlib import contextmanager
|
|
3
4
|
from functools import wraps
|
|
4
|
-
from typing import Generator
|
|
5
5
|
|
|
6
6
|
from httpdbg.hooks.utils import decorate
|
|
7
7
|
from httpdbg.hooks.utils import getcallargs
|
|
@@ -1,21 +1,30 @@
|
|
|
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
|
|
7
|
-
from typing import Union
|
|
8
7
|
import uuid
|
|
9
8
|
|
|
10
9
|
from httpdbg import HTTPRecords
|
|
11
10
|
from httpdbg.env import HTTPDBG_MULTIPROCESS_DIR
|
|
12
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
|
|
13
15
|
from httpdbg.log import logger
|
|
14
16
|
|
|
15
17
|
HTTPDBG_INITIATORS = [] # type: ignore
|
|
16
18
|
HTTPDBG_RECORD_SERVER = False
|
|
17
19
|
|
|
18
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
|
+
|
|
19
28
|
class HttpdbgRecorder:
|
|
20
29
|
"""Records the HTTP requests in subprocesses and saves them to disk."""
|
|
21
30
|
|
|
@@ -24,8 +33,7 @@ class HttpdbgRecorder:
|
|
|
24
33
|
self.fname: str = os.path.join(
|
|
25
34
|
os.environ[HTTPDBG_MULTIPROCESS_DIR], str(uuid.uuid1())
|
|
26
35
|
)
|
|
27
|
-
self.
|
|
28
|
-
self.records: Union[HTTPRecords, None] = None
|
|
36
|
+
self.serialized: list[str] = []
|
|
29
37
|
|
|
30
38
|
def _save_to_disk_loop(self):
|
|
31
39
|
while self._running:
|
|
@@ -45,19 +53,39 @@ class HttpdbgRecorder:
|
|
|
45
53
|
time.sleep(1)
|
|
46
54
|
|
|
47
55
|
def save_to_disk(self):
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
f"{self.fname}.
|
|
53
|
-
|
|
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())
|
|
54
82
|
|
|
55
83
|
def start(self):
|
|
56
84
|
"""Start recording."""
|
|
57
85
|
self.context = httprecord(
|
|
58
86
|
initiators=HTTPDBG_INITIATORS, server=HTTPDBG_RECORD_SERVER
|
|
59
87
|
)
|
|
60
|
-
self.records = self.context.__enter__()
|
|
88
|
+
self.records: HTTPRecords = self.context.__enter__()
|
|
61
89
|
self._running = True
|
|
62
90
|
self._thread = threading.Thread(target=self._save_to_disk_loop, daemon=True)
|
|
63
91
|
self._thread.start()
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
from collections.abc import Callable
|
|
2
|
+
from collections.abc import Generator
|
|
2
3
|
from contextlib import contextmanager
|
|
3
4
|
import inspect
|
|
4
5
|
import os
|
|
5
6
|
import typing
|
|
6
|
-
from typing import Generator
|
|
7
7
|
|
|
8
8
|
from httpdbg.initiator import httpdbg_group
|
|
9
9
|
from httpdbg.initiator import httpdbg_tag
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from collections.abc import Callable
|
|
2
|
+
from collections.abc import Generator
|
|
2
3
|
from contextlib import contextmanager
|
|
3
4
|
import datetime
|
|
4
5
|
import typing
|
|
5
|
-
from typing import Generator
|
|
6
6
|
|
|
7
7
|
from httpdbg.hooks.utils import decorate
|
|
8
8
|
from httpdbg.hooks.utils import undecorate
|