httpdbg 0.28.2__tar.gz → 0.29.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 (77) hide show
  1. {httpdbg-0.28.2 → httpdbg-0.29.0}/PKG-INFO +2 -2
  2. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/__init__.py +1 -1
  3. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/env.py +1 -0
  4. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/hooks/aiohttp.py +5 -9
  5. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/hooks/external.py +3 -1
  6. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/hooks/httpx.py +9 -17
  7. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/hooks/pytest.py +29 -1
  8. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/hooks/requests.py +5 -9
  9. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/hooks/socket.py +66 -46
  10. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/hooks/urllib3.py +5 -9
  11. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/initiator.py +55 -16
  12. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/records.py +32 -22
  13. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/webapp/api.py +16 -3
  14. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/webapp/static/api.js +11 -2
  15. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/webapp/static/configuration.js +3 -3
  16. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/webapp/static/filter.js +14 -14
  17. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/webapp/static/index.htm +10 -10
  18. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/webapp/static/main.js +1 -1
  19. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/webapp/static/render.js +17 -11
  20. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/webapp/static/style.css +1 -1
  21. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/webapp/static/table.css +6 -6
  22. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg.egg-info/PKG-INFO +2 -2
  23. httpdbg-0.29.0/httpdbg.egg-info/requires.txt +3 -0
  24. {httpdbg-0.28.2 → httpdbg-0.29.0}/pyproject.toml +1 -1
  25. {httpdbg-0.28.2 → httpdbg-0.29.0}/tests/test_hook_aiohttp.py +3 -15
  26. {httpdbg-0.28.2 → httpdbg-0.29.0}/tests/test_hook_httpx.py +6 -19
  27. {httpdbg-0.28.2 → httpdbg-0.29.0}/tests/test_hook_requests.py +3 -6
  28. {httpdbg-0.28.2 → httpdbg-0.29.0}/tests/test_hook_urllib3.py +32 -30
  29. httpdbg-0.29.0/tests/test_initiator.py +167 -0
  30. {httpdbg-0.28.2 → httpdbg-0.29.0}/tests/test_mode_module_pytest.py +54 -5
  31. {httpdbg-0.28.2 → httpdbg-0.29.0}/tests/test_mode_script.py +5 -12
  32. httpdbg-0.28.2/httpdbg.egg-info/requires.txt +0 -3
  33. httpdbg-0.28.2/tests/test_initiator.py +0 -172
  34. {httpdbg-0.28.2 → httpdbg-0.29.0}/LICENSE +0 -0
  35. {httpdbg-0.28.2 → httpdbg-0.29.0}/README.md +0 -0
  36. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/__main__.py +0 -0
  37. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/args.py +0 -0
  38. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/exception.py +0 -0
  39. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/hooks/__init__.py +0 -0
  40. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/hooks/all.py +0 -0
  41. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/hooks/generic.py +0 -0
  42. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/hooks/utils.py +0 -0
  43. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/log.py +0 -0
  44. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/mode_console.py +0 -0
  45. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/mode_module.py +0 -0
  46. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/mode_script.py +0 -0
  47. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/preview.py +0 -0
  48. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/server.py +0 -0
  49. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/utils.py +0 -0
  50. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/webapp/__init__.py +0 -0
  51. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/webapp/app.py +0 -0
  52. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/webapp/static/column.css +0 -0
  53. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/webapp/static/detail.css +0 -0
  54. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/webapp/static/favicon.ico +0 -0
  55. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/webapp/static/icons/chevron-direction-top-outline-icon.svg +0 -0
  56. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/webapp/static/icons/download-install-line-icon.svg +0 -0
  57. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/webapp/static/icons/engine-gears-icon.svg +0 -0
  58. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/webapp/static/icons/files-papers-outline-icon.svg +0 -0
  59. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/webapp/static/icons/filter-outline-icon.svg +0 -0
  60. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/webapp/static/icons/information-mark-circle-outline-icon.svg +0 -0
  61. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/webapp/static/icons/math-multiplication-icon.svg +0 -0
  62. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/webapp/static/icons/push-pin-green-icon.svg +0 -0
  63. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/webapp/static/icons/question-mark-round-line-icon.svg +0 -0
  64. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/webapp/static/icons/recycle-bin-line-icon.svg +0 -0
  65. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/webapp/static/icons/wait-sandclock-icon.svg +0 -0
  66. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg/webapp/static/mustache.min.js +0 -0
  67. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg.egg-info/SOURCES.txt +0 -0
  68. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg.egg-info/dependency_links.txt +0 -0
  69. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg.egg-info/entry_points.txt +0 -0
  70. {httpdbg-0.28.2 → httpdbg-0.29.0}/httpdbg.egg-info/top_level.txt +0 -0
  71. {httpdbg-0.28.2 → httpdbg-0.29.0}/setup.cfg +0 -0
  72. {httpdbg-0.28.2 → httpdbg-0.29.0}/tests/test_api.py +0 -0
  73. {httpdbg-0.28.2 → httpdbg-0.29.0}/tests/test_cli.py +0 -0
  74. {httpdbg-0.28.2 → httpdbg-0.29.0}/tests/test_context_manager.py +0 -0
  75. {httpdbg-0.28.2 → httpdbg-0.29.0}/tests/test_mode_console.py +0 -0
  76. {httpdbg-0.28.2 → httpdbg-0.29.0}/tests/test_preview.py +0 -0
  77. {httpdbg-0.28.2 → httpdbg-0.29.0}/tests/test_server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: httpdbg
3
- Version: 0.28.2
3
+ Version: 0.29.0
4
4
  Summary: A very simple tool to debug HTTP(S) client requests
5
5
  Author-email: cle-b <cle@tictac.pm>
6
6
  License: Apache-2.0
@@ -24,7 +24,7 @@ Requires-Python: >=3.9
24
24
  Description-Content-Type: text/markdown
25
25
  License-File: LICENSE
26
26
  Provides-Extra: pytest
27
- Requires-Dist: pytest-httpdbg>=0.3.1; extra == "pytest"
27
+ Requires-Dist: pytest-httpdbg>=0.6.0; extra == "pytest"
28
28
 
29
29
  # httpdbg
30
30
 
@@ -3,6 +3,6 @@ from httpdbg.hooks.all import httprecord
3
3
  from httpdbg.records import HTTPRecords
4
4
 
5
5
 
6
- __version__ = "0.28.2"
6
+ __version__ = "0.29.0"
7
7
 
8
8
  __all__ = ["httprecord", "HTTPRecords"]
@@ -1,3 +1,4 @@
1
+ HTTPDBG_CURRENT_GROUP = "HTTPDBG_CURRENT_GROUP"
1
2
  HTTPDBG_CURRENT_INITIATOR = "HTTPDBG_CURRENT_INITIATOR"
2
3
  HTTPDBG_CURRENT_TAG = "HTTPDBG_CURRENT_TAG"
3
4
  HTTPDBG_SUBPROCESS_DIR = "HTTPDBG_SUBPROCESS_DIR"
@@ -1,4 +1,5 @@
1
1
  # -*- coding: utf-8 -*-
2
+ from collections.abc import Callable
2
3
  from contextlib import contextmanager
3
4
  import traceback
4
5
  from typing import Generator
@@ -7,11 +8,10 @@ from httpdbg.hooks.utils import getcallargs
7
8
  from httpdbg.hooks.utils import decorate
8
9
  from httpdbg.hooks.utils import undecorate
9
10
  from httpdbg.initiator import httpdbg_initiator
10
- from httpdbg.records import HTTPRecord
11
11
  from httpdbg.records import HTTPRecords
12
12
 
13
13
 
14
- def set_hook_for_aiohttp_async(records, method):
14
+ def set_hook_for_aiohttp_async(records: HTTPRecords, method: Callable):
15
15
  async def hook(*args, **kwargs):
16
16
  initiator = None
17
17
  try:
@@ -25,13 +25,9 @@ def set_hook_for_aiohttp_async(records, method):
25
25
 
26
26
  if "str_or_url" in callargs:
27
27
  if initiator:
28
- record = HTTPRecord()
29
-
30
- record.initiator = initiator
31
- record.url = str(callargs["str_or_url"])
32
- record.exception = ex
33
-
34
- records.requests[record.id] = record
28
+ records.add_new_record_exception(
29
+ initiator, str(callargs["str_or_url"]), ex
30
+ )
35
31
  raise
36
32
 
37
33
  return hook
@@ -46,8 +46,10 @@ class WatcherSubprocessDirThread(threading.Thread):
46
46
  def load_dump(self):
47
47
  for dump in glob.glob(f"{os.environ[HTTPDBG_SUBPROCESS_DIR]}/*.httpdbgrecords"):
48
48
  with open(dump, "rb") as dumpfile:
49
- newrecords = pickle.load(dumpfile)
49
+ newrecords: HTTPRecords = pickle.load(dumpfile)
50
50
  self.records.requests.update(newrecords.requests)
51
+ self.records.initiators.update(newrecords.initiators)
52
+ self.records.groups.update(newrecords.groups)
51
53
 
52
54
  def run(self):
53
55
  while not self.stop:
@@ -1,4 +1,5 @@
1
1
  # -*- coding: utf-8 -*-
2
+ from collections.abc import Callable
2
3
  from contextlib import contextmanager
3
4
  import traceback
4
5
  from typing import Generator
@@ -7,11 +8,10 @@ from httpdbg.hooks.utils import getcallargs
7
8
  from httpdbg.hooks.utils import decorate
8
9
  from httpdbg.hooks.utils import undecorate
9
10
  from httpdbg.initiator import httpdbg_initiator
10
- from httpdbg.records import HTTPRecord
11
11
  from httpdbg.records import HTTPRecords
12
12
 
13
13
 
14
- def set_hook_for_httpx_async(records, method):
14
+ def set_hook_for_httpx_async(records: HTTPRecords, method: Callable):
15
15
  async def hook(*args, **kwargs):
16
16
  initiator = None
17
17
  try:
@@ -25,19 +25,15 @@ def set_hook_for_httpx_async(records, method):
25
25
 
26
26
  if "url" in callargs:
27
27
  if initiator:
28
- record = HTTPRecord()
29
-
30
- record.initiator = initiator
31
- record.url = str(callargs["url"])
32
- record.exception = ex
33
-
34
- records.requests[record.id] = record
28
+ records.add_new_record_exception(
29
+ initiator, str(callargs["url"]), ex
30
+ )
35
31
  raise
36
32
 
37
33
  return hook
38
34
 
39
35
 
40
- def set_hook_for_httpx(records, method):
36
+ def set_hook_for_httpx(records: HTTPRecords, method: Callable):
41
37
  def hook(*args, **kwargs):
42
38
  initiator = None
43
39
  try:
@@ -51,13 +47,9 @@ def set_hook_for_httpx(records, method):
51
47
 
52
48
  if "url" in callargs:
53
49
  if initiator:
54
- record = HTTPRecord()
55
-
56
- record.initiator = initiator
57
- record.url = str(callargs["url"])
58
- record.exception = ex
59
-
60
- records.requests[record.id] = record
50
+ records.add_new_record_exception(
51
+ initiator, str(callargs["url"]), ex
52
+ )
61
53
  raise
62
54
 
63
55
  return hook
@@ -1,16 +1,20 @@
1
1
  # -*- coding: utf-8 -*-
2
+ from collections.abc import Callable
2
3
  from contextlib import contextmanager
3
4
  from typing import Generator
4
5
 
5
6
  from httpdbg.hooks.utils import decorate
6
7
  from httpdbg.hooks.utils import getcallargs
7
8
  from httpdbg.hooks.utils import undecorate
9
+ from httpdbg.initiator import httpdbg_group
8
10
  from httpdbg.initiator import httpdbg_tag
11
+ from httpdbg.log import logger
9
12
  from httpdbg.records import HTTPRecords
10
13
 
11
14
 
12
- def set_hook_for_pytest_fixture(_, method):
15
+ def set_hook_for_pytest_fixture(_: HTTPRecords, method: Callable):
13
16
  def hook(*args, **kwargs):
17
+ logger().info("SET_HOOK_FOR_PYTEST_FIXTURE")
14
18
  callargs = getcallargs(method, *args, **kwargs)
15
19
 
16
20
  if "fixturefunc" in callargs:
@@ -22,11 +26,28 @@ def set_hook_for_pytest_fixture(_, method):
22
26
  return hook
23
27
 
24
28
 
29
+ def set_hook_for_pytest_runtest(records: HTTPRecords, method: Callable):
30
+ def hook(*args, **kwargs):
31
+ logger().info("SET_HOOK_FOR_PYTEST_RUNTEST")
32
+ callargs = getcallargs(method, *args, **kwargs)
33
+
34
+ if "item" in callargs:
35
+ full_label = getattr(callargs["item"], "nodeid", "unknown test")
36
+ label = getattr(callargs["item"], "name", "unknown test")
37
+ with httpdbg_group(records, label, full_label):
38
+ return method(*args, **kwargs)
39
+ else:
40
+ return method(*args, **kwargs)
41
+
42
+ return hook
43
+
44
+
25
45
  @contextmanager
26
46
  def hook_pytest(records: HTTPRecords) -> Generator[None, None, None]:
27
47
  hooks = False
28
48
  try:
29
49
  import _pytest.fixtures
50
+ import _pytest.runner
30
51
 
31
52
  _pytest.fixtures.call_fixture_func = decorate(
32
53
  records, _pytest.fixtures.call_fixture_func, set_hook_for_pytest_fixture
@@ -36,6 +57,11 @@ def hook_pytest(records: HTTPRecords) -> Generator[None, None, None]:
36
57
  _pytest.fixtures._teardown_yield_fixture,
37
58
  set_hook_for_pytest_fixture,
38
59
  )
60
+
61
+ _pytest.runner.runtestprotocol = decorate(
62
+ records, _pytest.runner.runtestprotocol, set_hook_for_pytest_runtest
63
+ )
64
+
39
65
  hooks = True
40
66
  except ImportError:
41
67
  pass
@@ -49,3 +75,5 @@ def hook_pytest(records: HTTPRecords) -> Generator[None, None, None]:
49
75
  _pytest.fixtures._teardown_yield_fixture = undecorate(
50
76
  _pytest.fixtures._teardown_yield_fixture
51
77
  )
78
+
79
+ _pytest.runner.runtestprotocol = undecorate(_pytest.runner.runtestprotocol)
@@ -1,4 +1,5 @@
1
1
  # -*- coding: utf-8 -*-
2
+ from collections.abc import Callable
2
3
  from contextlib import contextmanager
3
4
  import traceback
4
5
  from typing import Generator
@@ -7,11 +8,10 @@ from httpdbg.hooks.utils import getcallargs
7
8
  from httpdbg.hooks.utils import decorate
8
9
  from httpdbg.hooks.utils import undecorate
9
10
  from httpdbg.initiator import httpdbg_initiator
10
- from httpdbg.records import HTTPRecord
11
11
  from httpdbg.records import HTTPRecords
12
12
 
13
13
 
14
- def set_hook_for_requests(records, method):
14
+ def set_hook_for_requests(records: HTTPRecords, method: Callable):
15
15
  def hook(*args, **kwargs):
16
16
  initiator = None
17
17
  try:
@@ -25,13 +25,9 @@ def set_hook_for_requests(records, method):
25
25
 
26
26
  if "url" in callargs:
27
27
  if initiator:
28
- record = HTTPRecord()
29
-
30
- record.initiator = initiator
31
- record.url = str(callargs["url"])
32
- record.exception = ex
33
-
34
- records.requests[record.id] = record
28
+ records.add_new_record_exception(
29
+ initiator, str(callargs["url"]), ex
30
+ )
35
31
  raise
36
32
 
37
33
  return hook
@@ -1,6 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  import asyncio
3
3
  import asyncio.proactor_events
4
+ from collections.abc import Callable
4
5
  from contextlib import contextmanager
5
6
  import platform
6
7
  import socket
@@ -16,7 +17,10 @@ from httpdbg.records import HTTPRecords
16
17
  from httpdbg.log import logger
17
18
 
18
19
 
19
- def set_hook_for_socket_init(records, method):
20
+ # hook: socket.socket.__init__
21
+ # what: A new socket object is created.
22
+ # action: If an entry exists in the temporary raw socket storage list, it is removed.
23
+ def set_hook_for_socket_init(records: HTTPRecords, method: Callable):
20
24
  def hook(self, *args, **kwargs):
21
25
  records.del_socket_data(self)
22
26
 
@@ -25,27 +29,24 @@ def set_hook_for_socket_init(records, method):
25
29
  return hook
26
30
 
27
31
 
28
- def set_hook_for_socket_connect(records, method):
32
+ # hook: socket.socket.connect, ssl.SSLSocket.connect
33
+ # what: A connection to a remote socket is initiated.
34
+ # action: A new entry is added to the temporary raw socket storage list.
35
+ def set_hook_for_socket_connect(records: HTTPRecords, method: Callable):
29
36
  def hook(self, address):
30
37
  socketdata = records.get_socket_data(self, force_new=True)
31
38
  if socketdata:
32
39
  logger().info(
33
40
  f"CONNECT - self={self} id={id(self)} socketdata={socketdata} address={address}"
34
41
  )
35
-
36
42
  try:
37
43
  r = method(self, address)
38
44
  except Exception as ex:
39
45
  if not isinstance(
40
46
  ex, (BlockingIOError, OSError)
41
47
  ): # BlockingIOError for async, OSError for ipv6
42
- record = HTTPRecord()
43
- record.initiator = records.get_initiator()
44
-
45
- record.exception = ex
46
-
47
- records.requests[record.id] = record
48
-
48
+ initiator = records.get_initiator()
49
+ records.add_new_record_exception(initiator, "", ex)
49
50
  raise
50
51
 
51
52
  return r
@@ -53,18 +54,17 @@ def set_hook_for_socket_connect(records, method):
53
54
  return hook
54
55
 
55
56
 
56
- def set_hook_for_ssl_wrap_socket(records, method):
57
+ # hook: ssl.wrap_socket,
58
+ # what: Takes an instance sock of socket.socket, and returns an instance of ssl.SSLSocket.
59
+ # a subtype of socket.socket, which wraps the underlying socket in an SSL context.
60
+ # action: Link the socket and the sslsocket
61
+ def set_hook_for_ssl_wrap_socket(records: HTTPRecords, method: Callable):
57
62
  def hook(sock, *args, **kwargs):
58
63
  try:
59
64
  sslsocket = method(sock, *args, **kwargs)
60
65
  except Exception as ex:
61
- record = HTTPRecord()
62
-
63
- record.initiator = records.get_initiator()
64
- record.exception = ex
65
-
66
- records.requests[record.id] = record
67
-
66
+ initiator = records.get_initiator()
67
+ records.add_new_record_exception(initiator, "", ex)
68
68
  raise
69
69
 
70
70
  logger().info(
@@ -80,18 +80,16 @@ def set_hook_for_ssl_wrap_socket(records, method):
80
80
  return hook
81
81
 
82
82
 
83
- def set_hook_for_sslcontext_wrap_socket(records, method):
83
+ # hook: ssl.SSLContext.wrap_socket
84
+ # what: Wrap an existing Python socket sock and return an instance of SSLContext.sslsocket_class (default SSLSocket).
85
+ # action: Link the socket and the sslsocket
86
+ def set_hook_for_sslcontext_wrap_socket(records: HTTPRecords, method: Callable):
84
87
  def hook(self, sock, *args, **kwargs):
85
88
  try:
86
89
  sslsocket = method(self, sock, *args, **kwargs)
87
90
  except Exception as ex:
88
- record = HTTPRecord()
89
-
90
- record.initiator = records.get_initiator()
91
- record.exception = ex
92
-
93
- records.requests[record.id] = record
94
-
91
+ initiator = records.get_initiator()
92
+ records.add_new_record_exception(initiator, "", ex)
95
93
  raise
96
94
 
97
95
  logger().info(
@@ -107,18 +105,16 @@ def set_hook_for_sslcontext_wrap_socket(records, method):
107
105
  return hook
108
106
 
109
107
 
110
- def set_hook_for_socket_wrap_bio(records, method):
108
+ # hook: ssl.SSLContext.wrap_bio
109
+ # what: Wrap the BIO objects incoming and outgoing and return an instance of SSLContext.sslobject_class (default SSLObject).
110
+ # action: Record a new SocketRawData if necessary
111
+ def set_hook_for_socket_wrap_bio(records: HTTPRecords, method: Callable):
111
112
  def hook(self, *args, **kwargs):
112
113
  try:
113
114
  sslobject = method(self, *args, **kwargs)
114
115
  except Exception as ex:
115
- record = HTTPRecord()
116
- record.initiator = records.get_initiator()
117
-
118
- record.exception = ex
119
-
120
- records.requests[record.id] = record
121
-
116
+ initiator = records.get_initiator()
117
+ records.add_new_record_exception(initiator, "", ex)
122
118
  raise
123
119
 
124
120
  logger().info(
@@ -134,7 +130,10 @@ def set_hook_for_socket_wrap_bio(records, method):
134
130
  return hook
135
131
 
136
132
 
137
- def set_hook_for_socket_recv_into(records, method):
133
+ # hook: socket.socket.recv_into, ssl.SSLSocket.recv_into, asyncio.proactor_events._ProactorReadPipeTransport._data_received
134
+ # what: Receive up to nbytes bytes from the socket, storing the data into a buffer rather than creating a new bytestring.
135
+ # action: Append the data to an existing SocketRawData
136
+ def set_hook_for_socket_recv_into(records: HTTPRecords, method: Callable):
138
137
  def hook(self, buffer, *args, **kwargs):
139
138
  socketdata = records.get_socket_data(self)
140
139
  if socketdata:
@@ -160,7 +159,10 @@ def set_hook_for_socket_recv_into(records, method):
160
159
  return hook
161
160
 
162
161
 
163
- def set_hook_for_socket_recv(records, method):
162
+ # hook: socket.socket.recv, ssl.SSLSocket.recv
163
+ # what: Receive data from the socket. The return value is a bytes object representing the data received.
164
+ # action: Append the data to an existing SocketRawData
165
+ def set_hook_for_socket_recv(records: HTTPRecords, method: Callable):
164
166
  def hook(self, bufsize, *args, **kwargs):
165
167
  socketdata = records.get_socket_data(self)
166
168
  if socketdata:
@@ -183,7 +185,11 @@ def set_hook_for_socket_recv(records, method):
183
185
  return hook
184
186
 
185
187
 
186
- def set_hook_for_socket_sendall(records, method):
188
+ # hook: socket.socket.sendall
189
+ # what: Send data to the socket.
190
+ # action: Append the data to an existing SocketRawData. Check if this is an HTTP request.
191
+ # and record it if this is case, otherwise delete the temporay SocketRawData.
192
+ def set_hook_for_socket_sendall(records: HTTPRecords, method: Callable):
187
193
  def hook(self, bytes, *args, **kwargs):
188
194
  socketdata = records.get_socket_data(self, request=True)
189
195
  if socketdata:
@@ -200,7 +206,7 @@ def set_hook_for_socket_sendall(records, method):
200
206
  if http_detected:
201
207
  logger().info("SENDALL - http detected")
202
208
  socketdata.record = HTTPRecord(tbegin=socketdata.tbegin)
203
- socketdata.record.initiator = records.get_initiator()
209
+ socketdata.record.initiator_id = records.get_initiator()
204
210
  socketdata.record.address = socketdata.address
205
211
  socketdata.record.ssl = socketdata.ssl
206
212
  socketdata.record.request.rawdata = socketdata.rawdata
@@ -218,7 +224,11 @@ def set_hook_for_socket_sendall(records, method):
218
224
  return hook
219
225
 
220
226
 
221
- def set_hook_for_socket_send(records, method):
227
+ # hook: socket.socket.send, ssl.SSLSocket.send, asyncio.proactor_events._ProactorBaseWritePipeTransport.write
228
+ # what: Send data to the socket.
229
+ # action: Append the data to an existing SocketRawData. Check if this is an HTTP request.
230
+ # and record it if this is case, otherwise delete the temporay SocketRawData.
231
+ def set_hook_for_socket_send(records: HTTPRecords, method: Callable):
222
232
  def hook(self, bytes, *args, **kwargs):
223
233
  socketdata = records.get_socket_data(self, request=True)
224
234
  if socketdata:
@@ -241,7 +251,7 @@ def set_hook_for_socket_send(records, method):
241
251
  http_detected = socketdata.http_detected()
242
252
  if http_detected:
243
253
  socketdata.record = HTTPRecord(tbegin=socketdata.tbegin)
244
- socketdata.record.initiator = records.get_initiator()
254
+ socketdata.record.initiator_id = records.get_initiator()
245
255
  socketdata.record.address = socketdata.address
246
256
  socketdata.record.ssl = socketdata.ssl
247
257
  socketdata.record.request.rawdata = socketdata.rawdata
@@ -253,7 +263,10 @@ def set_hook_for_socket_send(records, method):
253
263
  return hook
254
264
 
255
265
 
256
- def set_hook_for_asyncio_create_connection(records, method):
266
+ # hook: asyncio.BaseEventLoop.create_connection
267
+ # what: Open a streaming transport connection to a given address specified by host and port.
268
+ # action: Link the socket and the sslsocket
269
+ def set_hook_for_asyncio_create_connection(records: HTTPRecords, method: Callable):
257
270
  async def hook(self, *args, **kwargs):
258
271
  logger().info(
259
272
  f"CREATE_CONNECTION - self={self} id={id(self)} args={args} kwargs={kwargs}"
@@ -276,7 +289,11 @@ def set_hook_for_asyncio_create_connection(records, method):
276
289
  return hook
277
290
 
278
291
 
279
- def set_hook_for_sslobject_write(records, method):
292
+ # hook: ssl.SSLObject.write
293
+ # what: Write buf to the SSL socket and return the number of bytes written.
294
+ # action: Append the data to an existing SocketRawData. Check if this is an HTTP request.
295
+ # and record it if this is case, otherwise delete the temporay SocketRawData.
296
+ def set_hook_for_sslobject_write(records: HTTPRecords, method: Callable):
280
297
  def hook(self, buf, *args, **kwargs):
281
298
  logger().info(f"WRITE - {type(self)}={id(self)} buf={(b'' + buf)[:20]}")
282
299
  socketdata = records.get_socket_data(self, request=True)
@@ -298,7 +315,7 @@ def set_hook_for_sslobject_write(records, method):
298
315
  http_detected = socketdata.http_detected()
299
316
  if http_detected:
300
317
  socketdata.record = HTTPRecord(tbegin=socketdata.tbegin)
301
- socketdata.record.initiator = records.get_initiator()
318
+ socketdata.record.initiator_id = records.get_initiator()
302
319
  socketdata.record.address = socketdata.address
303
320
  socketdata.record.ssl = socketdata.ssl
304
321
  socketdata.record.request.rawdata = socketdata.rawdata
@@ -310,7 +327,10 @@ def set_hook_for_sslobject_write(records, method):
310
327
  return hook
311
328
 
312
329
 
313
- def set_hook_for_sslobject_read(records, method):
330
+ # hook: ssl.SSLObject.read
331
+ # what: Read up to len bytes of data from the SSL socket and return the result as a bytes instance.
332
+ # action: Append the data to an existing SocketRawData.
333
+ def set_hook_for_sslobject_read(records: HTTPRecords, method: Callable):
314
334
  def hook(self, *args, **kwargs):
315
335
  logger().info(f"READ - {type(self)}={id(self)}")
316
336
  socketdata = records.get_socket_data(self)
@@ -393,7 +413,7 @@ def hook_socket(records: HTTPRecords) -> Generator[None, None, None]:
393
413
  )
394
414
 
395
415
  # only for async HTTP requests (not HTTPS) on Windows
396
- if (platform.system().lower() == "windows") and (sys.version_info >= (3, 7)):
416
+ if platform.system().lower() == "windows":
397
417
  asyncio.proactor_events._ProactorReadPipeTransport._data_received = decorate( # type: ignore
398
418
  records,
399
419
  asyncio.proactor_events._ProactorReadPipeTransport._data_received, # type: ignore
@@ -435,7 +455,7 @@ def hook_socket(records: HTTPRecords) -> Generator[None, None, None]:
435
455
  )
436
456
 
437
457
  # only for async HTTP requests (not HTTPS) on Windows
438
- if (platform.system().lower() == "windows") and (sys.version_info >= (3, 7)):
458
+ if platform.system().lower() == "windows":
439
459
  asyncio.proactor_events._ProactorReadPipeTransport._data_received = undecorate( # type: ignore
440
460
  asyncio.proactor_events._ProactorReadPipeTransport._data_received # type: ignore
441
461
  )
@@ -1,4 +1,5 @@
1
1
  # -*- coding: utf-8 -*-
2
+ from collections.abc import Callable
2
3
  from contextlib import contextmanager
3
4
  import traceback
4
5
  from typing import Generator
@@ -7,11 +8,10 @@ from httpdbg.hooks.utils import getcallargs
7
8
  from httpdbg.hooks.utils import decorate
8
9
  from httpdbg.hooks.utils import undecorate
9
10
  from httpdbg.initiator import httpdbg_initiator
10
- from httpdbg.records import HTTPRecord
11
11
  from httpdbg.records import HTTPRecords
12
12
 
13
13
 
14
- def set_hook_for_urllib3(records, method):
14
+ def set_hook_for_urllib3(records: HTTPRecords, method: Callable):
15
15
  def hook(*args, **kwargs):
16
16
  initiator = None
17
17
  try:
@@ -25,13 +25,9 @@ def set_hook_for_urllib3(records, method):
25
25
 
26
26
  if "url" in callargs:
27
27
  if initiator:
28
- record = HTTPRecord()
29
-
30
- record.initiator = initiator
31
- record.url = str(callargs["url"])
32
- record.exception = ex
33
-
34
- records.requests[record.id] = record
28
+ records.add_new_record_exception(
29
+ initiator, str(callargs["url"]), ex
30
+ )
35
31
  raise
36
32
 
37
33
  return hook
@@ -7,7 +7,12 @@ from typing import Generator
7
7
  from typing import List
8
8
  from typing import Tuple
9
9
  from typing import Union
10
+ from typing import TYPE_CHECKING
10
11
 
12
+ if TYPE_CHECKING:
13
+ from httpdbg.records import HTTPRecords
14
+
15
+ from httpdbg.env import HTTPDBG_CURRENT_GROUP
11
16
  from httpdbg.env import HTTPDBG_CURRENT_INITIATOR
12
17
  from httpdbg.env import HTTPDBG_CURRENT_TAG
13
18
  from httpdbg.hooks.utils import getcallargs
@@ -15,26 +20,23 @@ from httpdbg.utils import get_new_uuid
15
20
  from httpdbg.log import logger
16
21
 
17
22
 
18
- class Initiator(object):
23
+ class Initiator:
19
24
  def __init__(
20
25
  self,
21
26
  id: str,
22
- short_label: str,
23
- long_label: Union[str, None],
27
+ label: str,
24
28
  short_stack: str,
25
29
  stack: List[str],
26
30
  ):
27
31
  self.id = id
28
- self.short_label = short_label
29
- self.long_label = long_label
32
+ self.label = label
30
33
  self.short_stack = short_stack
31
34
  self.stack = stack
32
35
 
33
36
  def __eq__(self, other) -> bool:
34
37
  if type(other) is Initiator:
35
38
  return (
36
- self.short_label == other.short_label
37
- and self.long_label == other.long_label
39
+ self.label == other.label
38
40
  and self.short_stack == other.short_stack
39
41
  and self.stack == other.stack
40
42
  )
@@ -45,21 +47,29 @@ class Initiator(object):
45
47
  if full:
46
48
  json = {
47
49
  "id": self.id,
48
- "short_label": self.short_label,
49
- "long_label": self.long_label,
50
+ "label": self.label,
50
51
  "short_stack": self.short_stack,
51
52
  "stack": "\n".join(self.stack),
52
53
  }
53
54
  else:
54
55
  json = {
55
56
  "id": self.id,
56
- "short_label": self.short_label,
57
- "long_label": self.long_label,
57
+ "label": self.label,
58
58
  "short_stack": self.short_stack,
59
59
  }
60
60
  return json
61
61
 
62
62
 
63
+ class Group:
64
+ def __init__(self, id: str, label: str, full_label: str):
65
+ self.id = id
66
+ self.label = label
67
+ self.full_label = full_label
68
+
69
+ def to_json(self) -> dict:
70
+ return {"id": self.id, "label": self.label, "full_label": self.full_label}
71
+
72
+
63
73
  def compatible_path(path: str) -> str:
64
74
  p = path
65
75
  if platform.system().lower() == "windows":
@@ -205,15 +215,16 @@ def httpdbg_initiator(
205
215
  short_stack += f" {k}={v}\n"
206
216
  short_stack += ")"
207
217
 
208
- current_initiator = Initiator(
209
- get_new_uuid(), instruction, None, short_stack, stack
210
- )
211
- records._initiators[current_initiator.id] = current_initiator
218
+ current_initiator = Initiator(get_new_uuid(), instruction, short_stack, stack)
219
+ records.initiators[current_initiator.id] = current_initiator
212
220
 
213
221
  os.environ[envname] = current_initiator.id
214
222
 
215
223
  try:
216
- yield current_initiator
224
+ with httpdbg_group(
225
+ records, instruction, short_stack
226
+ ): # by default we group the requests by initiator
227
+ yield current_initiator
217
228
  except Exception:
218
229
  del os.environ[envname]
219
230
  raise
@@ -233,6 +244,7 @@ def httpdbg_tag(tag: str) -> Generator[None, None, None]:
233
244
  os.environ[HTTPDBG_CURRENT_TAG] = tag
234
245
 
235
246
  try:
247
+ logger().info(f"httpdbg_tag {tag}")
236
248
  yield
237
249
  except Exception:
238
250
  if (not tag_already_set) and (HTTPDBG_CURRENT_TAG in os.environ):
@@ -241,3 +253,30 @@ def httpdbg_tag(tag: str) -> Generator[None, None, None]:
241
253
 
242
254
  if (not tag_already_set) and (HTTPDBG_CURRENT_TAG in os.environ):
243
255
  del os.environ[HTTPDBG_CURRENT_TAG]
256
+
257
+
258
+ @contextmanager
259
+ def httpdbg_group(
260
+ records: "HTTPRecords", label: str, full_label: str
261
+ ) -> Generator[None, None, None]:
262
+
263
+ group_already_set = HTTPDBG_CURRENT_GROUP in os.environ
264
+
265
+ if not group_already_set:
266
+ logger().info("httpdbg_group (new)")
267
+ group_id = get_new_uuid()
268
+ records.groups[group_id] = Group(group_id, label, full_label)
269
+ os.environ[HTTPDBG_CURRENT_GROUP] = group_id
270
+
271
+ try:
272
+ logger().info(
273
+ f"httpdbg_group {records.groups} group_id={os.environ[HTTPDBG_CURRENT_GROUP]} label={label} full_label={full_label}"
274
+ )
275
+ yield
276
+ except Exception:
277
+ if (not group_already_set) and (HTTPDBG_CURRENT_GROUP in os.environ):
278
+ del os.environ[HTTPDBG_CURRENT_GROUP]
279
+ raise
280
+
281
+ if (not group_already_set) and (HTTPDBG_CURRENT_GROUP in os.environ):
282
+ del os.environ[HTTPDBG_CURRENT_GROUP]