httpdbg 1.2.6__tar.gz → 2.0.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 (99) hide show
  1. {httpdbg-1.2.6 → httpdbg-2.0.0}/PKG-INFO +13 -1
  2. {httpdbg-1.2.6 → httpdbg-2.0.0}/README.md +12 -0
  3. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/__init__.py +1 -1
  4. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/args.py +1 -2
  5. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/hooks/all.py +25 -26
  6. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/hooks/external.py +1 -2
  7. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/hooks/fastapi.py +14 -11
  8. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/hooks/flask.py +10 -7
  9. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/hooks/generic.py +5 -6
  10. httpdbg-2.0.0/httpdbg/hooks/h2.py +220 -0
  11. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/hooks/httpx.py +22 -1
  12. httpdbg-2.0.0/httpdbg/hooks/record.py +179 -0
  13. httpdbg-2.0.0/httpdbg/hooks/recordhttp1.py +197 -0
  14. httpdbg-2.0.0/httpdbg/hooks/recordhttp2.py +113 -0
  15. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/hooks/socket.py +178 -36
  16. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/hooks/unittest.py +31 -25
  17. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/hooks/utils.py +3 -1
  18. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/hooks/uvicorn.py +19 -10
  19. httpdbg-2.0.0/httpdbg/http_status_code.py +93 -0
  20. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/initiator.py +8 -11
  21. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/mode_console.py +1 -2
  22. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/mode_module.py +1 -2
  23. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/mode_script.py +1 -2
  24. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/preview.py +11 -5
  25. httpdbg-2.0.0/httpdbg/records.py +92 -0
  26. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/server.py +2 -2
  27. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/utils.py +4 -5
  28. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/webapp/api.py +3 -4
  29. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/webapp/app.py +26 -17
  30. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg.egg-info/PKG-INFO +13 -1
  31. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg.egg-info/SOURCES.txt +6 -0
  32. {httpdbg-1.2.6 → httpdbg-2.0.0}/pyproject.toml +1 -0
  33. httpdbg-2.0.0/tests/test_hook_h2.py +315 -0
  34. {httpdbg-1.2.6 → httpdbg-2.0.0}/tests/test_server_hook_fastapi.py +4 -0
  35. {httpdbg-1.2.6 → httpdbg-2.0.0}/tests/test_server_hook_flask.py +10 -0
  36. httpdbg-1.2.6/httpdbg/records.py +0 -482
  37. {httpdbg-1.2.6 → httpdbg-2.0.0}/LICENSE +0 -0
  38. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/__main__.py +0 -0
  39. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/env.py +0 -0
  40. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/exception.py +0 -0
  41. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/hooks/__init__.py +0 -0
  42. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/hooks/aiohttp.py +0 -0
  43. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/hooks/http.py +0 -0
  44. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/hooks/pytest.py +0 -0
  45. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/hooks/requests.py +0 -0
  46. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/hooks/starlette.py +0 -0
  47. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/hooks/template_sitecustomize.py +0 -0
  48. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/hooks/urllib3.py +0 -0
  49. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/log.py +0 -0
  50. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/webapp/__init__.py +0 -0
  51. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/webapp/static/api.js +0 -0
  52. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/webapp/static/column.css +0 -0
  53. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/webapp/static/configuration.css +0 -0
  54. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/webapp/static/configuration.js +0 -0
  55. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/webapp/static/detail.css +0 -0
  56. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/webapp/static/favicon.ico +0 -0
  57. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/webapp/static/filter.js +0 -0
  58. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/webapp/static/icons/chevron-direction-top-outline-icon.svg +0 -0
  59. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/webapp/static/icons/download-install-line-icon.svg +0 -0
  60. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/webapp/static/icons/engine-gears-icon.svg +0 -0
  61. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/webapp/static/icons/eye-blind-icon.svg +0 -0
  62. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/webapp/static/icons/eye-icon.svg +0 -0
  63. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/webapp/static/icons/files-papers-outline-icon.svg +0 -0
  64. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/webapp/static/icons/filter-outline-icon.svg +0 -0
  65. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/webapp/static/icons/information-mark-circle-outline-icon.svg +0 -0
  66. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/webapp/static/icons/lock-line-icon.svg +0 -0
  67. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/webapp/static/icons/lock-open-line-icon.svg +0 -0
  68. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/webapp/static/icons/math-multiplication-icon.svg +0 -0
  69. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/webapp/static/icons/push-pin-green-icon.svg +0 -0
  70. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/webapp/static/icons/question-mark-round-line-icon.svg +0 -0
  71. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/webapp/static/icons/recycle-bin-line-icon.svg +0 -0
  72. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/webapp/static/icons/wait-sandclock-icon.svg +0 -0
  73. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/webapp/static/index.htm +0 -0
  74. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/webapp/static/main.js +0 -0
  75. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/webapp/static/mustache.min.js +0 -0
  76. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/webapp/static/render.js +0 -0
  77. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/webapp/static/style.css +0 -0
  78. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg/webapp/static/table.css +0 -0
  79. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg.egg-info/dependency_links.txt +0 -0
  80. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg.egg-info/entry_points.txt +0 -0
  81. {httpdbg-1.2.6 → httpdbg-2.0.0}/httpdbg.egg-info/top_level.txt +0 -0
  82. {httpdbg-1.2.6 → httpdbg-2.0.0}/setup.cfg +0 -0
  83. {httpdbg-1.2.6 → httpdbg-2.0.0}/tests/test_api.py +0 -0
  84. {httpdbg-1.2.6 → httpdbg-2.0.0}/tests/test_cli.py +0 -0
  85. {httpdbg-1.2.6 → httpdbg-2.0.0}/tests/test_context_manager.py +0 -0
  86. {httpdbg-1.2.6 → httpdbg-2.0.0}/tests/test_hook_aiohttp.py +0 -0
  87. {httpdbg-1.2.6 → httpdbg-2.0.0}/tests/test_hook_httpx.py +0 -0
  88. {httpdbg-1.2.6 → httpdbg-2.0.0}/tests/test_hook_pytest.py +0 -0
  89. {httpdbg-1.2.6 → httpdbg-2.0.0}/tests/test_hook_requests.py +0 -0
  90. {httpdbg-1.2.6 → httpdbg-2.0.0}/tests/test_hook_unittest.py +0 -0
  91. {httpdbg-1.2.6 → httpdbg-2.0.0}/tests/test_hook_urllib3.py +0 -0
  92. {httpdbg-1.2.6 → httpdbg-2.0.0}/tests/test_initiator.py +0 -0
  93. {httpdbg-1.2.6 → httpdbg-2.0.0}/tests/test_mode_console.py +0 -0
  94. {httpdbg-1.2.6 → httpdbg-2.0.0}/tests/test_mode_module_pytest.py +0 -0
  95. {httpdbg-1.2.6 → httpdbg-2.0.0}/tests/test_mode_script.py +0 -0
  96. {httpdbg-1.2.6 → httpdbg-2.0.0}/tests/test_multiprocess.py +0 -0
  97. {httpdbg-1.2.6 → httpdbg-2.0.0}/tests/test_preview.py +0 -0
  98. {httpdbg-1.2.6 → httpdbg-2.0.0}/tests/test_pyhttpdbg_server.py +0 -0
  99. {httpdbg-1.2.6 → httpdbg-2.0.0}/tests/test_server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: httpdbg
3
- Version: 1.2.6
3
+ Version: 2.0.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: Apache-2.0
@@ -32,6 +32,8 @@ To use it, execute your program using the `pyhttpdbg` command instead of `python
32
32
 
33
33
  ![](https://github.com/cle-b/httpdbg/blob/main/ui.png?raw=true)
34
34
 
35
+ Supports `HTTP/1.0`, `HTTP/1.1`, and `HTTP/2`.
36
+
35
37
  Full documentation => [https://httpdbg.readthedocs.io/](https://httpdbg.readthedocs.io/en/latest/)
36
38
 
37
39
  ## installation
@@ -156,6 +158,16 @@ The requests are grouped by tests.
156
158
  | pytest | supported | - | yes |
157
159
  | unittest | supported | - | yes |
158
160
 
161
+ ### supported HTTP version
162
+
163
+ | version | status |
164
+ |----------------|-------------------------------------------------------------|
165
+ | 1.0 | supported |
166
+ | 1.1 | supported |
167
+ | 2 | supported (only through the `h2` lib) |
168
+
169
+
170
+
159
171
  ## configuration
160
172
 
161
173
  No configuration is necessary to start but some few settings are available for particular use.
@@ -6,6 +6,8 @@ To use it, execute your program using the `pyhttpdbg` command instead of `python
6
6
 
7
7
  ![](https://github.com/cle-b/httpdbg/blob/main/ui.png?raw=true)
8
8
 
9
+ Supports `HTTP/1.0`, `HTTP/1.1`, and `HTTP/2`.
10
+
9
11
  Full documentation => [https://httpdbg.readthedocs.io/](https://httpdbg.readthedocs.io/en/latest/)
10
12
 
11
13
  ## installation
@@ -130,6 +132,16 @@ The requests are grouped by tests.
130
132
  | pytest | supported | - | yes |
131
133
  | unittest | supported | - | yes |
132
134
 
135
+ ### supported HTTP version
136
+
137
+ | version | status |
138
+ |----------------|-------------------------------------------------------------|
139
+ | 1.0 | supported |
140
+ | 1.1 | supported |
141
+ | 2 | supported (only through the `h2` lib) |
142
+
143
+
144
+
133
145
  ## configuration
134
146
 
135
147
  No configuration is necessary to start but some few settings are available for particular use.
@@ -3,6 +3,6 @@ from httpdbg.hooks.all import httprecord
3
3
  from httpdbg.records import HTTPRecords
4
4
 
5
5
 
6
- __version__ = "1.2.6"
6
+ __version__ = "2.0.0"
7
7
 
8
8
  __all__ = ["httprecord", "HTTPRecords"]
@@ -1,12 +1,11 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  import argparse
3
3
  from pathlib import Path
4
- from typing import List, Tuple
5
4
 
6
5
  from httpdbg.log import LogLevel
7
6
 
8
7
 
9
- def read_args(args: List[str]) -> Tuple[argparse.Namespace, List[str]]:
8
+ def read_args(args: list[str]) -> tuple[argparse.Namespace, list[str]]:
10
9
  httpdbg_args = args
11
10
  client_args = []
12
11
  for action in ["--console", "--module", "-m", "--script"]:
@@ -1,8 +1,5 @@
1
- # -*- coding: utf-8 -*-
2
1
  from contextlib import contextmanager
3
2
  from typing import Generator
4
- from typing import List
5
- from typing import Tuple
6
3
  from typing import Union
7
4
 
8
5
  from httpdbg.hooks.aiohttp import hook_aiohttp
@@ -10,6 +7,7 @@ from httpdbg.hooks.external import watcher_external
10
7
  from httpdbg.hooks.fastapi import hook_fastapi
11
8
  from httpdbg.hooks.flask import hook_flask
12
9
  from httpdbg.hooks.generic import hook_generic
10
+ from httpdbg.hooks.h2 import hook_h2
13
11
  from httpdbg.hooks.http import hook_http
14
12
  from httpdbg.hooks.httpx import hook_httpx
15
13
  from httpdbg.hooks.pytest import hook_pytest
@@ -26,10 +24,10 @@ from httpdbg.records import HTTPRecords
26
24
  @contextmanager
27
25
  def httprecord(
28
26
  records: HTTPRecords = None,
29
- initiators: Union[List[str], None] = None,
27
+ initiators: Union[list[str], None] = None,
30
28
  client: bool = True,
31
29
  server: bool = False,
32
- ignore: Union[List[Tuple[str, int]], None] = None,
30
+ ignore: tuple[tuple[str, int], ...] = (),
33
31
  multiprocess: bool = True,
34
32
  ) -> Generator[HTTPRecords, None, None]:
35
33
  if records is None:
@@ -37,25 +35,26 @@ def httprecord(
37
35
 
38
36
  with hook_flask(records):
39
37
  with hook_socket(records):
40
- with hook_fastapi(records):
41
- with hook_starlette(records):
42
- with hook_uvicorn(records):
43
- with hook_http(records):
44
- with hook_httpx(records):
45
- with hook_requests(records):
46
- with hook_urllib3(records):
47
- with hook_aiohttp(records):
48
- with hook_pytest(records):
49
- with hook_unittest(records):
50
- with hook_generic(
51
- records, initiators
52
- ):
53
- if multiprocess:
54
- with watcher_external(
55
- records,
56
- initiators,
57
- server,
58
- ):
38
+ with hook_h2(records):
39
+ with hook_fastapi(records):
40
+ with hook_starlette(records):
41
+ with hook_uvicorn(records):
42
+ with hook_http(records):
43
+ with hook_httpx(records):
44
+ with hook_requests(records):
45
+ with hook_urllib3(records):
46
+ with hook_aiohttp(records):
47
+ with hook_pytest(records):
48
+ with hook_unittest(records):
49
+ with hook_generic(
50
+ records, initiators
51
+ ):
52
+ if multiprocess:
53
+ with watcher_external(
54
+ records,
55
+ initiators,
56
+ server,
57
+ ):
58
+ yield records
59
+ else:
59
60
  yield records
60
- else:
61
- yield records
@@ -7,7 +7,6 @@ import pickle
7
7
  import tempfile
8
8
  import time
9
9
  import threading
10
- from typing import List
11
10
  from typing import Union
12
11
 
13
12
  from httpdbg.env import HTTPDBG_MULTIPROCESS_DIR
@@ -18,7 +17,7 @@ from httpdbg.records import HTTPRecords
18
17
  @contextmanager
19
18
  def watcher_external(
20
19
  records: HTTPRecords,
21
- initiators: Union[List[str], None] = None,
20
+ initiators: Union[list[str], None] = None,
22
21
  server: bool = False,
23
22
  ) -> Generator[HTTPRecords, None, None]:
24
23
  if HTTPDBG_MULTIPROCESS_DIR not in os.environ:
@@ -3,9 +3,7 @@ from collections.abc import Callable
3
3
  from contextlib import contextmanager
4
4
  import functools
5
5
  from functools import wraps
6
- from typing import Dict
7
6
  from typing import Generator
8
- from typing import Union
9
7
 
10
8
  import datetime
11
9
 
@@ -37,7 +35,7 @@ def set_hook_fastapi_endpoint(records: HTTPRecords, method: Callable):
37
35
  def set_hook_fastapi_apirouter_add_api_route(
38
36
  records: HTTPRecords,
39
37
  method: Callable,
40
- already_mapped: Union[Dict[Callable, Callable], None] = None,
38
+ already_mapped: dict[Callable, Callable],
41
39
  ):
42
40
 
43
41
  @wraps(method)
@@ -56,7 +54,9 @@ def set_hook_fastapi_apirouter_add_api_route(
56
54
  else:
57
55
  callargs[param] = already_mapped[original_view_func]
58
56
 
59
- args = [callargs[param] if x == original_view_func else x for x in args]
57
+ args = tuple(
58
+ callargs[param] if x == original_view_func else x for x in args
59
+ )
60
60
  if param in kwargs:
61
61
  kwargs[param] = callargs[param]
62
62
 
@@ -85,12 +85,15 @@ def set_hook_fastapi_app(records: HTTPRecords, method: Callable):
85
85
  *args,
86
86
  **kwargs,
87
87
  ) as group:
88
- if group and (group.id in records.groups):
89
- if socketdata_id and (socketdata_id in records._sockets):
90
- records.groups[group.id].tbegin = records._sockets[
91
- socketdata_id
92
- ].record.tbegin - datetime.timedelta(milliseconds=1)
93
- records._sockets[socketdata_id].record.group_id = group.id
88
+ if group and socketdata_id:
89
+ sock = records._tracerhttp1.sockets.get(socketdata_id)
90
+ if (sock is not None) and (sock.record is not None):
91
+ record = records.groups.get(group.id)
92
+ if record is not None:
93
+ record.tbegin = sock.record.tbegin - datetime.timedelta(
94
+ milliseconds=1
95
+ )
96
+ sock.record.group_id = group.id
94
97
 
95
98
  ret = await method(*args, **kwargs)
96
99
 
@@ -121,7 +124,7 @@ def hook_fastapi(records: HTTPRecords) -> Generator[None, None, None]:
121
124
  try:
122
125
  import fastapi.routing
123
126
 
124
- already_mapped: Dict[Callable, Callable] = {}
127
+ already_mapped: dict[Callable, Callable] = {}
125
128
 
126
129
  set_hook_fastapi_apirouter_add_api_route_with_already_mapped = (
127
130
  functools.partial(
@@ -3,9 +3,8 @@ from collections.abc import Callable
3
3
  from contextlib import contextmanager
4
4
  import functools
5
5
  from functools import wraps
6
- from typing import Dict
7
6
  from typing import Generator
8
- from typing import Union
7
+
9
8
 
10
9
  from httpdbg.hooks.utils import getcallargs
11
10
  from httpdbg.hooks.utils import decorate
@@ -36,7 +35,7 @@ def set_hook_flask_endpoint(records: HTTPRecords, method: Callable):
36
35
  def set_hook_flask_add_url_rule(
37
36
  records: HTTPRecords,
38
37
  method: Callable,
39
- already_mapped: Union[Dict[Callable, Callable], None] = None,
38
+ already_mapped: dict[Callable, Callable],
40
39
  ):
41
40
 
42
41
  def hook(*args, **kwargs):
@@ -55,7 +54,9 @@ def set_hook_flask_add_url_rule(
55
54
  else:
56
55
  callargs[param] = already_mapped[original_view_func]
57
56
 
58
- args = [callargs[param] if x == original_view_func else x for x in args]
57
+ args = tuple(
58
+ callargs[param] if x == original_view_func else x for x in args
59
+ )
59
60
  if param in kwargs:
60
61
  kwargs[param] = callargs[param]
61
62
 
@@ -67,7 +68,7 @@ def set_hook_flask_add_url_rule(
67
68
  def set_hook_flask_register_error_handler(
68
69
  records: HTTPRecords,
69
70
  method: Callable,
70
- already_mapped: Union[Dict[Callable, Callable], None] = None,
71
+ already_mapped: dict[Callable, Callable],
71
72
  ):
72
73
 
73
74
  def hook(*args, **kwargs):
@@ -86,7 +87,9 @@ def set_hook_flask_register_error_handler(
86
87
  else:
87
88
  callargs[param] = already_mapped[original_view_func]
88
89
 
89
- args = [callargs[param] if x == original_view_func else x for x in args]
90
+ args = tuple(
91
+ callargs[param] if x == original_view_func else x for x in args
92
+ )
90
93
  if param in kwargs:
91
94
  kwargs[param] = callargs[param]
92
95
 
@@ -103,7 +106,7 @@ def hook_flask(records: HTTPRecords) -> Generator[None, None, None]:
103
106
 
104
107
  # we must not apply the hook more than once on a mapped endpoint function
105
108
  # AssertionError: View function mapping is overwriting an existing endpoint function: xxxx
106
- already_mapped: Dict[Callable, Callable] = {}
109
+ already_mapped: dict[Callable, Callable] = {}
107
110
 
108
111
  set_hook_flask_add_url_rule_with_already_mapped = functools.partial(
109
112
  set_hook_flask_add_url_rule, already_mapped=already_mapped
@@ -9,7 +9,6 @@ import traceback
9
9
  from types import ModuleType
10
10
  from typing import Any
11
11
  from typing import Generator
12
- from typing import List
13
12
  from typing import Union
14
13
 
15
14
  from httpdbg.hooks.utils import decorate
@@ -49,13 +48,13 @@ def set_hook_for_generic(records: HTTPRecords, method: Any):
49
48
 
50
49
  @contextmanager
51
50
  def hook_generic(
52
- records: HTTPRecords, initiators: Union[List[str], None] = None
51
+ records: HTTPRecords, initiators: Union[list[str], None] = None
53
52
  ) -> Generator[None, None, None]:
54
53
  if initiators:
55
54
  # we add a hook for a generic initiator only if the module is imported
56
55
 
57
- hooks: List[Callable] = []
58
- already_hooked: List[str] = []
56
+ hooks: list[Callable] = []
57
+ already_hooked: list[str] = []
59
58
 
60
59
  original_builtin_import = builtins.__import__
61
60
 
@@ -130,7 +129,7 @@ def hook_generic(
130
129
  fnc = undecorate(fnc)
131
130
 
132
131
 
133
- def list_callables_from_module(records: HTTPRecords, module: str) -> List[Any]:
132
+ def list_callables_from_module(records: HTTPRecords, module: str) -> list[Any]:
134
133
  callables = []
135
134
 
136
135
  try:
@@ -180,7 +179,7 @@ def list_callables_from_class(
180
179
  imported_module: ModuleType,
181
180
  module: str,
182
181
  classname: str,
183
- ) -> List[Any]:
182
+ ) -> list[Any]:
184
183
  callables = []
185
184
 
186
185
  try:
@@ -0,0 +1,220 @@
1
+ from collections.abc import Callable
2
+ from contextlib import contextmanager
3
+ import random
4
+ import traceback
5
+ from typing import Generator
6
+ from typing import Union
7
+
8
+ from httpdbg.hooks.recordhttp2 import HTTP2Record
9
+ from httpdbg.hooks.utils import decorate
10
+ from httpdbg.hooks.utils import getcallargs
11
+ from httpdbg.hooks.utils import undecorate
12
+ from httpdbg.initiator import httpdbg_initiator
13
+ from httpdbg.log import logger
14
+ from httpdbg.records import HTTPRecords
15
+
16
+
17
+ class TracerHTTP2:
18
+
19
+ def __init__(
20
+ self,
21
+ ignore: tuple[tuple[str, int], ...] = (),
22
+ ):
23
+ self.sockets: dict[str, HTTP2Record] = dict()
24
+ self.ignore: tuple[tuple[str, int], ...] = ignore
25
+
26
+ def send_headers(
27
+ self,
28
+ initiator_id: str,
29
+ group_id: str,
30
+ socket_id: int,
31
+ stream_id: int,
32
+ headers: list[tuple[bytes, bytes]],
33
+ ) -> HTTP2Record:
34
+
35
+ if f"{socket_id}-{stream_id}" in self.sockets:
36
+ record = self.sockets[f"{socket_id}-{stream_id}"]
37
+ if record.is_client:
38
+ # this is a new request: we "close" the previous one
39
+ self.sockets.pop(f"{socket_id}-{stream_id}")
40
+
41
+ record = HTTP2Record(initiator_id, group_id)
42
+ record.request.headers = headers
43
+ self.sockets[f"{socket_id}-{stream_id}"] = record
44
+ else:
45
+ record.response.headers = headers
46
+ else:
47
+ record = HTTP2Record(initiator_id, group_id)
48
+ record.request.headers = headers
49
+ self.sockets[f"{socket_id}-{stream_id}"] = record
50
+
51
+ return record
52
+
53
+ def send_data(
54
+ self,
55
+ socket_id: int,
56
+ stream_id: int,
57
+ data: bytes,
58
+ ):
59
+ record = self.sockets.get(f"{socket_id}-{stream_id}")
60
+ if record:
61
+ record.send_data(data)
62
+
63
+ def receive_headers(
64
+ self,
65
+ initiator_id: str,
66
+ group_id: str,
67
+ socket_id: int,
68
+ stream_id: int,
69
+ headers: list[tuple[bytes, bytes]],
70
+ is_client: bool = True,
71
+ ) -> Union[HTTP2Record, None]:
72
+ record = None
73
+ if not is_client:
74
+ if f"{socket_id}-{stream_id}" in self.sockets:
75
+ # this is a new request received by the server: we "close" the previous one
76
+ self.sockets.pop(f"{socket_id}-{stream_id}")
77
+ record = HTTP2Record(initiator_id, group_id, is_client=False)
78
+ record.request.headers = headers
79
+ self.sockets[f"{socket_id}-{stream_id}"] = record
80
+ else:
81
+ if f"{socket_id}-{stream_id}" in self.sockets:
82
+ record = self.sockets[f"{socket_id}-{stream_id}"]
83
+ record.response.headers = headers
84
+ return record
85
+
86
+ def receive_data(
87
+ self,
88
+ socket_id: int,
89
+ stream_id: int,
90
+ data: bytes,
91
+ ):
92
+ record = self.sockets.get(f"{socket_id}-{stream_id}")
93
+ if record:
94
+ record.receive_data(data)
95
+
96
+
97
+ def set_hook_for_h2_send_headers(records: HTTPRecords, method: Callable):
98
+ def hook(*args, **kwargs):
99
+ callargs = getcallargs(method, *args, **kwargs)
100
+ self = callargs.get("self")
101
+ stream_id = callargs.get("stream_id")
102
+ headers = callargs.get("headers")
103
+
104
+ with httpdbg_initiator(
105
+ records, traceback.extract_stack(), method, *args, **kwargs
106
+ ) as initiator_and_group:
107
+ initiator, group, _ = initiator_and_group
108
+ if all(x is not None for x in (self, stream_id, headers)):
109
+ logger().debug(
110
+ f"H2 send_headers - self={id(self)} steam_id={stream_id} headers={headers}"
111
+ )
112
+ record = records._tracerhttp2.send_headers(
113
+ initiator.id, group.id, id(self), stream_id, headers
114
+ )
115
+ if record.is_client and records.client:
116
+ records.requests[record.id] = record
117
+ ret = method(*args, **kwargs)
118
+ return ret
119
+
120
+ return hook
121
+
122
+
123
+ def set_hook_for_h2_send_data(records: HTTPRecords, method: Callable):
124
+ def hook(*args, **kwargs):
125
+ callargs = getcallargs(method, *args, **kwargs)
126
+ self = callargs.get("self")
127
+ stream_id = callargs.get("stream_id")
128
+ data = callargs.get("data")
129
+
130
+ with httpdbg_initiator(
131
+ records, traceback.extract_stack(), method, *args, **kwargs
132
+ ):
133
+ if all(x is not None for x in (self, stream_id, data)):
134
+ logger().debug(
135
+ f"H2 send_data - self={id(self)} steam_id={stream_id} data={data[:20]!r}"
136
+ )
137
+ records._tracerhttp2.send_data(id(self), stream_id, data)
138
+ ret = method(*args, **kwargs)
139
+ return ret
140
+
141
+ return hook
142
+
143
+
144
+ def set_hook_for_h2_receive_data(records: HTTPRecords, method: Callable):
145
+ def hook(*args, **kwargs):
146
+ callargs = getcallargs(method, *args, **kwargs)
147
+ self = callargs.get("self")
148
+
149
+ with httpdbg_initiator(
150
+ records, traceback.extract_stack(), method, *args, **kwargs
151
+ ) as initiator_and_group:
152
+ initiator, group, _ = initiator_and_group
153
+ ret = method(*args, **kwargs)
154
+
155
+ for event in ret:
156
+ import h2.events
157
+
158
+ if isinstance(event, h2.events.RequestReceived):
159
+ stream_id = event.stream_id
160
+ headers = event.headers
161
+ if all(x is not None for x in (self, stream_id, headers)):
162
+ logger().debug(
163
+ f"H2 receive_data headers RequestReceived {random.random()} - self={id(self)} steam_id={stream_id} headers={headers}"
164
+ )
165
+ record = records._tracerhttp2.receive_headers(
166
+ initiator.id,
167
+ group.id,
168
+ id(self),
169
+ stream_id,
170
+ headers,
171
+ is_client=False,
172
+ )
173
+ if record and (record.is_client is False) and records.server:
174
+ records.requests[record.id] = record
175
+
176
+ elif isinstance(event, h2.events.ResponseReceived):
177
+ stream_id = event.stream_id
178
+ headers = event.headers
179
+ if all(x is not None for x in (self, stream_id, headers)):
180
+ logger().debug(
181
+ f"H2 receive_data headers ResponseReceived {random.random()} - self={id(self)} steam_id={stream_id} headers={headers}"
182
+ )
183
+ records._tracerhttp2.receive_headers(
184
+ initiator.id, group.id, id(self), stream_id, headers
185
+ )
186
+
187
+ elif isinstance(event, h2.events.DataReceived):
188
+ stream_id = event.stream_id
189
+ data = event.data
190
+ if all(x is not None for x in (self, stream_id, data)):
191
+ logger().debug(
192
+ f"H2 receive_data data - self={id(self)} steam_id={stream_id} data={data[:20]!r}"
193
+ )
194
+ records._tracerhttp2.receive_data(id(self), stream_id, data)
195
+
196
+ return ret
197
+
198
+ return hook
199
+
200
+
201
+ @contextmanager
202
+ def hook_h2(records: HTTPRecords) -> Generator[None, None, None]:
203
+ hooks = False
204
+ try:
205
+ import h2.connection
206
+
207
+ h2.connection.H2Connection.send_headers = decorate(records, h2.connection.H2Connection.send_headers, set_hook_for_h2_send_headers) # type: ignore[arg-type]
208
+ h2.connection.H2Connection.send_data = decorate(records, h2.connection.H2Connection.send_data, set_hook_for_h2_send_data) # type: ignore[arg-type]
209
+ h2.connection.H2Connection.receive_data = decorate(records, h2.connection.H2Connection.receive_data, set_hook_for_h2_receive_data) # type: ignore[arg-type]
210
+
211
+ hooks = True
212
+ except ImportError:
213
+ pass
214
+
215
+ yield
216
+
217
+ if hooks:
218
+ h2.connection.H2Connection.send_headers = undecorate(h2.connection.H2Connection.send_headers) # type: ignore[arg-type]
219
+ h2.connection.H2Connection.send_data = undecorate(h2.connection.H2Connection.send_data) # type: ignore[arg-type]
220
+ h2.connection.H2Connection.receive_data = undecorate(h2.connection.H2Connection.receive_data) # type: ignore[arg-type]
@@ -1,4 +1,3 @@
1
- # -*- coding: utf-8 -*-
2
1
  from collections.abc import Callable
3
2
  from contextlib import contextmanager
4
3
  import traceback
@@ -90,6 +89,19 @@ def hook_httpx(records: HTTPRecords) -> Generator[None, None, None]:
90
89
  records, httpx.AsyncClient.request, set_hook_for_httpx_async
91
90
  )
92
91
 
92
+ httpx.Client.get = decorate(records, httpx.Client.get, set_hook_for_httpx)
93
+ httpx.Client.post = decorate(records, httpx.Client.post, set_hook_for_httpx)
94
+ httpx.Client.patch = decorate(records, httpx.Client.patch, set_hook_for_httpx)
95
+ httpx.Client.put = decorate(records, httpx.Client.put, set_hook_for_httpx)
96
+ httpx.Client.delete = decorate(records, httpx.Client.delete, set_hook_for_httpx)
97
+ httpx.Client.head = decorate(records, httpx.Client.head, set_hook_for_httpx)
98
+ httpx.Client.options = decorate(
99
+ records, httpx.Client.options, set_hook_for_httpx
100
+ )
101
+ httpx.Client.request = decorate(
102
+ records, httpx.Client.request, set_hook_for_httpx
103
+ )
104
+
93
105
  httpx.get = decorate(records, httpx.get, set_hook_for_httpx)
94
106
  httpx.post = decorate(records, httpx.post, set_hook_for_httpx)
95
107
  httpx.patch = decorate(records, httpx.patch, set_hook_for_httpx)
@@ -115,6 +127,15 @@ def hook_httpx(records: HTTPRecords) -> Generator[None, None, None]:
115
127
  httpx.AsyncClient.options = undecorate(httpx.AsyncClient.options)
116
128
  httpx.AsyncClient.request = undecorate(httpx.AsyncClient.request)
117
129
 
130
+ httpx.Client.get = undecorate(httpx.Client.get)
131
+ httpx.Client.post = undecorate(httpx.Client.post)
132
+ httpx.Client.patch = undecorate(httpx.Client.patch)
133
+ httpx.Client.put = undecorate(httpx.Client.put)
134
+ httpx.Client.delete = undecorate(httpx.Client.delete)
135
+ httpx.Client.head = undecorate(httpx.Client.head)
136
+ httpx.Client.options = undecorate(httpx.Client.options)
137
+ httpx.Client.request = undecorate(httpx.Client.request)
138
+
118
139
  httpx.get = undecorate(httpx.get)
119
140
  httpx.post = undecorate(httpx.post)
120
141
  httpx.patch = undecorate(httpx.patch)