httpdbg 2.0.2__tar.gz → 2.1.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 (101) hide show
  1. {httpdbg-2.0.2 → httpdbg-2.1.0}/PKG-INFO +7 -1
  2. {httpdbg-2.0.2 → httpdbg-2.1.0}/README.md +6 -0
  3. httpdbg-2.1.0/httpdbg/__init__.py +20 -0
  4. httpdbg-2.1.0/httpdbg/__main__.py +93 -0
  5. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/args.py +9 -3
  6. httpdbg-2.1.0/httpdbg/export.py +109 -0
  7. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/webapp/app.py +19 -51
  8. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/webapp/static/api.js +34 -16
  9. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/webapp/static/index.htm +59 -46
  10. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg.egg-info/PKG-INFO +7 -1
  11. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg.egg-info/SOURCES.txt +1 -0
  12. httpdbg-2.0.2/httpdbg/__init__.py +0 -7
  13. httpdbg-2.0.2/httpdbg/__main__.py +0 -69
  14. {httpdbg-2.0.2 → httpdbg-2.1.0}/LICENSE +0 -0
  15. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/env.py +0 -0
  16. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/exception.py +0 -0
  17. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/hooks/__init__.py +0 -0
  18. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/hooks/aiohttp.py +0 -0
  19. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/hooks/all.py +0 -0
  20. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/hooks/external.py +0 -0
  21. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/hooks/fastapi.py +0 -0
  22. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/hooks/flask.py +0 -0
  23. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/hooks/generic.py +0 -0
  24. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/hooks/h2.py +0 -0
  25. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/hooks/http.py +0 -0
  26. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/hooks/httpx.py +0 -0
  27. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/hooks/pytest.py +0 -0
  28. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/hooks/record.py +0 -0
  29. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/hooks/recordhttp1.py +0 -0
  30. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/hooks/recordhttp2.py +0 -0
  31. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/hooks/requests.py +0 -0
  32. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/hooks/socket.py +0 -0
  33. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/hooks/starlette.py +0 -0
  34. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/hooks/template_sitecustomize.py +0 -0
  35. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/hooks/unittest.py +0 -0
  36. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/hooks/urllib3.py +0 -0
  37. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/hooks/utils.py +0 -0
  38. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/hooks/uvicorn.py +0 -0
  39. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/http_status_code.py +0 -0
  40. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/initiator.py +0 -0
  41. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/log.py +0 -0
  42. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/mode_console.py +0 -0
  43. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/mode_module.py +0 -0
  44. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/mode_script.py +0 -0
  45. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/preview.py +0 -0
  46. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/records.py +0 -0
  47. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/server.py +0 -0
  48. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/utils.py +0 -0
  49. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/webapp/__init__.py +0 -0
  50. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/webapp/api.py +0 -0
  51. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/webapp/static/column.css +0 -0
  52. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/webapp/static/configuration.css +0 -0
  53. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/webapp/static/configuration.js +0 -0
  54. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/webapp/static/detail.css +0 -0
  55. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/webapp/static/favicon.ico +0 -0
  56. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/webapp/static/filter.js +0 -0
  57. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/webapp/static/icons/chevron-direction-top-outline-icon.svg +0 -0
  58. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/webapp/static/icons/download-install-line-icon.svg +0 -0
  59. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/webapp/static/icons/engine-gears-icon.svg +0 -0
  60. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/webapp/static/icons/eye-blind-icon.svg +0 -0
  61. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/webapp/static/icons/eye-icon.svg +0 -0
  62. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/webapp/static/icons/files-papers-outline-icon.svg +0 -0
  63. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/webapp/static/icons/filter-outline-icon.svg +0 -0
  64. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/webapp/static/icons/information-mark-circle-outline-icon.svg +0 -0
  65. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/webapp/static/icons/lock-line-icon.svg +0 -0
  66. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/webapp/static/icons/lock-open-line-icon.svg +0 -0
  67. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/webapp/static/icons/math-multiplication-icon.svg +0 -0
  68. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/webapp/static/icons/push-pin-green-icon.svg +0 -0
  69. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/webapp/static/icons/question-mark-round-line-icon.svg +0 -0
  70. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/webapp/static/icons/recycle-bin-line-icon.svg +0 -0
  71. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/webapp/static/icons/wait-sandclock-icon.svg +0 -0
  72. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/webapp/static/main.js +0 -0
  73. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/webapp/static/mustache.min.js +0 -0
  74. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/webapp/static/render.js +0 -0
  75. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/webapp/static/style.css +0 -0
  76. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg/webapp/static/table.css +0 -0
  77. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg.egg-info/dependency_links.txt +0 -0
  78. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg.egg-info/entry_points.txt +0 -0
  79. {httpdbg-2.0.2 → httpdbg-2.1.0}/httpdbg.egg-info/top_level.txt +0 -0
  80. {httpdbg-2.0.2 → httpdbg-2.1.0}/pyproject.toml +0 -0
  81. {httpdbg-2.0.2 → httpdbg-2.1.0}/setup.cfg +0 -0
  82. {httpdbg-2.0.2 → httpdbg-2.1.0}/tests/test_api.py +0 -0
  83. {httpdbg-2.0.2 → httpdbg-2.1.0}/tests/test_cli.py +0 -0
  84. {httpdbg-2.0.2 → httpdbg-2.1.0}/tests/test_context_manager.py +0 -0
  85. {httpdbg-2.0.2 → httpdbg-2.1.0}/tests/test_hook_aiohttp.py +0 -0
  86. {httpdbg-2.0.2 → httpdbg-2.1.0}/tests/test_hook_h2.py +0 -0
  87. {httpdbg-2.0.2 → httpdbg-2.1.0}/tests/test_hook_httpx.py +0 -0
  88. {httpdbg-2.0.2 → httpdbg-2.1.0}/tests/test_hook_pytest.py +0 -0
  89. {httpdbg-2.0.2 → httpdbg-2.1.0}/tests/test_hook_requests.py +0 -0
  90. {httpdbg-2.0.2 → httpdbg-2.1.0}/tests/test_hook_unittest.py +0 -0
  91. {httpdbg-2.0.2 → httpdbg-2.1.0}/tests/test_hook_urllib3.py +0 -0
  92. {httpdbg-2.0.2 → httpdbg-2.1.0}/tests/test_initiator.py +0 -0
  93. {httpdbg-2.0.2 → httpdbg-2.1.0}/tests/test_mode_console.py +0 -0
  94. {httpdbg-2.0.2 → httpdbg-2.1.0}/tests/test_mode_module_pytest.py +0 -0
  95. {httpdbg-2.0.2 → httpdbg-2.1.0}/tests/test_mode_script.py +0 -0
  96. {httpdbg-2.0.2 → httpdbg-2.1.0}/tests/test_multiprocess.py +0 -0
  97. {httpdbg-2.0.2 → httpdbg-2.1.0}/tests/test_preview.py +0 -0
  98. {httpdbg-2.0.2 → httpdbg-2.1.0}/tests/test_pyhttpdbg_server.py +0 -0
  99. {httpdbg-2.0.2 → httpdbg-2.1.0}/tests/test_server.py +0 -0
  100. {httpdbg-2.0.2 → httpdbg-2.1.0}/tests/test_server_hook_fastapi.py +0 -0
  101. {httpdbg-2.0.2 → httpdbg-2.1.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.0.2
3
+ Version: 2.1.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
@@ -192,6 +192,8 @@ options:
192
192
  --only-client record only HTTP client requests
193
193
  --keep-up, -k keep the server up even if the requests have been read
194
194
  --force-quit, -q stop the server even if the requests have not been read
195
+ --export-html EXPORT_HTML
196
+ the path to the export file
195
197
  --console run a python console (default)
196
198
  --module MODULE, -m MODULE
197
199
  run library module as a script (the next args are passed to pytest as is)
@@ -225,6 +227,10 @@ The requests:
225
227
  * are still available in the web page even if the python process stopped (except if you force quit before the requests have been loaded by the web page).
226
228
  * are automatically cleaned if a new execution is detected.
227
229
 
230
+ ## export
231
+
232
+ You can export the HTTP traces in a single HTML file. In that case, the web interface is not available during the execution of the python command.
233
+
228
234
  ## limitations
229
235
 
230
236
  Theoretically, if your HTTP client or server uses a standard Python socket, the HTTP requests will be recorded.
@@ -166,6 +166,8 @@ options:
166
166
  --only-client record only HTTP client requests
167
167
  --keep-up, -k keep the server up even if the requests have been read
168
168
  --force-quit, -q stop the server even if the requests have not been read
169
+ --export-html EXPORT_HTML
170
+ the path to the export file
169
171
  --console run a python console (default)
170
172
  --module MODULE, -m MODULE
171
173
  run library module as a script (the next args are passed to pytest as is)
@@ -199,6 +201,10 @@ The requests:
199
201
  * are still available in the web page even if the python process stopped (except if you force quit before the requests have been loaded by the web page).
200
202
  * are automatically cleaned if a new execution is detected.
201
203
 
204
+ ## export
205
+
206
+ You can export the HTTP traces in a single HTML file. In that case, the web interface is not available during the execution of the python command.
207
+
202
208
  ## limitations
203
209
 
204
210
  Theoretically, if your HTTP client or server uses a standard Python socket, the HTTP requests will be recorded.
@@ -0,0 +1,20 @@
1
+ __version__ = "2.1.0"
2
+
3
+ __all__ = ["export_html", "httprecord", "HTTPRecords"]
4
+
5
+
6
+ # lazy loading to avoid circular import issue
7
+ def __getattr__(name):
8
+ if name == "export_html":
9
+ from httpdbg.export import export_html
10
+
11
+ return export_html
12
+ if name == "httprecord":
13
+ from httpdbg.hooks.all import httprecord
14
+
15
+ return httprecord
16
+ if name == "HTTPRecords":
17
+ from httpdbg.records import HTTPRecords
18
+
19
+ return HTTPRecords
20
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,93 @@
1
+ import argparse
2
+ from pathlib import Path
3
+ import sys
4
+ import time
5
+ from typing import Union
6
+
7
+ from httpdbg import __version__
8
+ from httpdbg.args import read_args
9
+ from httpdbg.export import export_html
10
+ from httpdbg.hooks.all import httprecord
11
+ from httpdbg.log import set_env_for_logging
12
+ from httpdbg.server import httpdbg_srv
13
+ from httpdbg.mode_console import run_console
14
+ from httpdbg.mode_module import run_module
15
+ from httpdbg.mode_script import run_script
16
+ from httpdbg.records import HTTPRecords
17
+
18
+
19
+ def print_msg(msg):
20
+ sep = ".... - - .--. -.. -... --. .... - - .--. -.. -... --. .... - - .--. -.. -... --."
21
+ msg = f"{sep}\n{msg}\n{sep}"
22
+ print(msg)
23
+
24
+
25
+ def pyhttpdbg(
26
+ params: argparse.Namespace, subparams: list[str], test_mode: bool = False
27
+ ):
28
+
29
+ set_env_for_logging(params.log_level, params.log)
30
+
31
+ url = f"http://{params.host}:{params.port}/{'?hi=on' if params.console else ''}"
32
+
33
+ if not params.no_banner:
34
+ print_msg(f" httpdbg - HTTP(S) requests available at {url}")
35
+
36
+ sys.path.insert(0, "") # to mimic the default python command behavior
37
+
38
+ def run_recorder(
39
+ records: HTTPRecords,
40
+ initiators: Union[list[str], None],
41
+ record_server: bool,
42
+ test_mode: bool,
43
+ ):
44
+ records.server = record_server
45
+ with httprecord(records, initiators, server=records.server):
46
+ if params.module:
47
+ run_module(subparams)
48
+ elif params.script:
49
+ run_script(subparams)
50
+ else:
51
+ run_console(records, test_mode)
52
+
53
+ if params.export_html:
54
+ records = HTTPRecords()
55
+
56
+ run_recorder(records, params.initiator, not params.only_client, test_mode)
57
+
58
+ export_html(records, Path(params.export_html))
59
+
60
+ else:
61
+ with httpdbg_srv(params.host, params.port) as records:
62
+
63
+ run_recorder(records, params.initiator, not params.only_client, test_mode)
64
+
65
+ if not (params.force_quit or test_mode):
66
+ if not params.no_banner:
67
+ print_msg(f" httpdbg - HTTP(S) requests available at {url}")
68
+
69
+ if params.keep_up:
70
+ input("Press enter to quit")
71
+ else:
72
+ # we keep the server up until all the requests have been loaded in the web interface
73
+ print(
74
+ "Waiting until all the requests have been loaded in the web interface."
75
+ )
76
+ print("Press Ctrl+C to quit.")
77
+ try:
78
+ while records.unread:
79
+ time.sleep(0.5)
80
+ except KeyboardInterrupt: # pragma: no cover
81
+ pass
82
+
83
+
84
+ def pyhttpdbg_entry_point(test_mode=False):
85
+ params, subparams = read_args(sys.argv[1:])
86
+ if params.version:
87
+ print(__version__)
88
+ else:
89
+ pyhttpdbg(params, subparams, test_mode=test_mode)
90
+
91
+
92
+ if __name__ == "__main__":
93
+ pyhttpdbg_entry_point()
@@ -40,22 +40,28 @@ def read_args(args: list[str]) -> tuple[argparse.Namespace, list[str]]:
40
40
  help="record only HTTP client requests",
41
41
  )
42
42
 
43
- server_state = parser.add_mutually_exclusive_group()
43
+ server_or_export = parser.add_mutually_exclusive_group()
44
44
 
45
- server_state.add_argument(
45
+ server_or_export.add_argument(
46
46
  "--keep-up",
47
47
  "-k",
48
48
  action="store_true",
49
49
  help="keep the server up even if the requests have been read",
50
50
  )
51
51
 
52
- server_state.add_argument(
52
+ server_or_export.add_argument(
53
53
  "--force-quit",
54
54
  "-q",
55
55
  action="store_true",
56
56
  help="stop the server even if the requests have not been read",
57
57
  )
58
58
 
59
+ server_or_export.add_argument(
60
+ "--export-html",
61
+ type=str,
62
+ help="the path to the export file",
63
+ )
64
+
59
65
  parser.add_argument(
60
66
  "--log-level",
61
67
  type=LogLevel,
@@ -0,0 +1,109 @@
1
+ import base64
2
+ import json
3
+ from pathlib import Path
4
+ import re
5
+ import xml.etree.ElementTree as ET
6
+
7
+ import httpdbg
8
+ from httpdbg import HTTPRecords
9
+ from httpdbg.webapp.api import RequestListPayload, RequestPayload
10
+
11
+
12
+ def generate_html(records: HTTPRecords, for_export: bool = True) -> str:
13
+
14
+ current_dir = Path(__file__).resolve().parent
15
+
16
+ with open(Path(current_dir) / "webapp/static/index.htm") as findexhtml:
17
+ html = findexhtml.read()
18
+
19
+ html = html.replace("$**HTTPDBG_VERSION**$", httpdbg.__version__)
20
+
21
+ # favicon
22
+ with open(current_dir / "webapp/static/favicon.ico", "rb") as ffavicon:
23
+ b64_icon = base64.b64encode(ffavicon.read()).decode("utf-8")
24
+ data_uri = f"data:image/x-icon;base64,{b64_icon}"
25
+ html = html.replace(
26
+ '<link rel="shortcut icon" href="static/favicon.ico">',
27
+ f'<link rel="icon" type="image/x-icon" href="{data_uri}">',
28
+ )
29
+
30
+ # icons
31
+ def svg_file_to_symbol(svg_path: Path, symbol_id: str) -> str:
32
+ ET.register_namespace("", "http://www.w3.org/2000/svg")
33
+ root = ET.parse(svg_path).getroot()
34
+ symbol = ET.Element(
35
+ "symbol", {"id": symbol_id, "viewBox": root.attrib["viewBox"]}
36
+ )
37
+ for child in list(root):
38
+ symbol.append(child)
39
+ return ET.tostring(symbol, encoding="unicode")
40
+
41
+ icons_inline = ""
42
+ for icon_path in (current_dir / "webapp/static/icons").glob("*.svg"):
43
+ icons_inline += "\n " + svg_file_to_symbol(
44
+ icon_path, icon_path.name[:-4]
45
+ )
46
+ icons_inline = f'<svg width="0" height="0" style="position:absolute;visibility:hidden" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">{icons_inline}\n </svg>'
47
+ html = html.replace("$**PRELOAD_ICONS**$", icons_inline)
48
+
49
+ # css
50
+ stylesheet_pattern = (
51
+ r'<link\s+[^>]*rel=["\']stylesheet["\'][^>]*href=["\']([^"\']+)["\']'
52
+ )
53
+ for stylesheet in re.findall(stylesheet_pattern, html):
54
+ with open(Path(current_dir) / "webapp" / stylesheet) as fcss:
55
+ html = html.replace(
56
+ f'<link rel="stylesheet" href="{stylesheet}">',
57
+ f"<style>{fcss.read()}</style>",
58
+ )
59
+
60
+ # js
61
+ javascript_pattern = r'<script\s+[^>]*src=["\']([^"\']+)["\']'
62
+ for js in re.findall(javascript_pattern, html):
63
+ with open(Path(current_dir) / "webapp" / js) as fjs:
64
+ html = html.replace(
65
+ f'<script src="{js}"></script>', f"<script>{fjs.read()}</script>"
66
+ )
67
+
68
+ # static export of the requests data
69
+ if for_export:
70
+ static_all_requests = json.dumps(
71
+ records, cls=RequestListPayload, ensure_ascii=False
72
+ )
73
+ map_requests: dict[str, object] = dict()
74
+ for record in records:
75
+ map_requests[record.id] = json.loads(json.dumps(record, cls=RequestPayload))
76
+ static_requests: str = json.dumps(map_requests, ensure_ascii=False)
77
+
78
+ def safe_for_script_tag(s: str) -> str:
79
+ return s.replace("</script>", "<\\/script>")
80
+
81
+ html_export = f"""
82
+ <script id="all-requests" type="application/json">
83
+ {safe_for_script_tag(static_all_requests)}
84
+ </script>
85
+
86
+ <script id="requests-map" type="application/json">
87
+ {safe_for_script_tag(static_requests)}
88
+ </script>
89
+
90
+ <script>
91
+ global.static_all_requests = JSON.parse(
92
+ document.getElementById('all-requests').textContent
93
+ );
94
+ global.static_requests = JSON.parse(
95
+ document.getElementById('requests-map').textContent
96
+ );
97
+ </script>
98
+ """
99
+
100
+ html = html.replace("$**EXPORT**$", html_export)
101
+ else:
102
+ html = html.replace("$**EXPORT**$", "")
103
+
104
+ return html
105
+
106
+
107
+ def export_html(records: HTTPRecords, filename: Path):
108
+ with open(filename, "w") as f:
109
+ f.write(generate_html(records))
@@ -1,18 +1,13 @@
1
- # -*- coding: utf-8 -*-
2
1
  from collections.abc import Callable
3
2
  from contextlib import contextmanager
4
- import glob
5
3
  from http.server import BaseHTTPRequestHandler
6
4
  import json
7
- import mimetypes
8
- import os
5
+ from pathlib import Path
9
6
  import re
10
7
  from urllib.parse import ParseResult
11
8
  from urllib.parse import parse_qs
12
9
  from urllib.parse import urlparse
13
10
 
14
-
15
- from httpdbg import __version__
16
11
  from httpdbg.log import logger
17
12
  from httpdbg.webapp.api import RequestListPayload, RequestPayload
18
13
  from httpdbg.records import HTTPRecords
@@ -56,61 +51,34 @@ class HttpbgHTTPRequestHandler(BaseHTTPRequestHandler):
56
51
  break
57
52
 
58
53
  def serve_static(self, url: ParseResult):
59
- base_path = os.path.dirname(os.path.realpath(__file__))
60
-
61
- if not (
62
- (url.path.lower() in ["/", "index.htm", "index.html"])
63
- or url.path.startswith("/static/")
64
- ):
65
- return False
66
54
 
67
55
  if url.path.lower() in ["/", "index.htm", "index.html"]:
68
- self.path = "/static/index.htm"
69
-
70
- # get real path
71
- self.path = self.path.split("-+-")[0]
72
-
73
- fullpath = os.path.realpath(os.path.join(base_path, self.path[1:]))
56
+ from httpdbg.export import generate_html
74
57
 
75
- if not fullpath.startswith(base_path):
76
- # if the file is not in the static directory, we don't serve it
77
- return self.serve_not_found(url)
78
-
79
- if not os.path.exists(fullpath):
80
- return self.serve_not_found(url)
81
- else:
82
58
  self.send_response(200)
83
- self.send_header(
84
- "content-type", mimetypes.types_map[os.path.splitext(fullpath)[1]]
85
- )
86
- if self.path == "/static/index.htm":
87
- self.send_header_no_cache()
88
- else:
89
- self.send_header_with_cache(604800)
59
+ self.send_header("content-type", "text/html")
60
+ self.send_header_no_cache()
90
61
  self.end_headers()
62
+ self.wfile.write(
63
+ generate_html(self.records, for_export=False).encode("utf-8")
64
+ )
65
+ return True
91
66
 
92
- with open(fullpath, "rb") as f:
93
- filecontent = f.read()
94
-
95
- if b"$**PRELOAD_ICONS**$" in filecontent:
96
- icons = ""
97
- for icon in glob.glob(
98
- os.path.realpath(base_path) + "/static/icons/*.svg"
99
- ):
100
- icon_path = icon.replace(os.path.realpath(base_path) + "/", "")
101
- icons += f" <link rel='preload' href='{icon_path}-+-$**HTTPDBG_VERSION**$' as='image' type='image/svg+xml' />\n"
67
+ if url.path.lower() == "favicon.ico":
68
+ self.send_response(200)
69
+ self.send_header("content-type", "image/x-icon")
70
+ self.send_header_no_cache()
71
+ self.end_headers()
102
72
 
103
- filecontent = filecontent.replace(
104
- b"$**PRELOAD_ICONS**$", icons.encode("utf-8")
105
- )
73
+ current_dir = Path(__file__).resolve().parent
106
74
 
107
- filecontent = filecontent.replace(
108
- b"$**HTTPDBG_VERSION**$", __version__.encode("utf-8")
109
- )
75
+ with open(Path(current_dir) / "static/favicon.ico") as f:
76
+ filecontent = f.read()
77
+ self.wfile.write(filecontent.encode("utf-8"))
110
78
 
111
- self.wfile.write(filecontent)
79
+ return True
112
80
 
113
- return True
81
+ return False
114
82
 
115
83
  def serve_requests(self, url: ParseResult):
116
84
  if not (url.path.lower() == "/requests"):
@@ -5,7 +5,7 @@ const global = {
5
5
  sessions: {},
6
6
  requests: {},
7
7
  initiators: {},
8
- groups: {},
8
+ groups: {},
9
9
  connected: false,
10
10
  group_collapse: [],
11
11
  groupby: "default"
@@ -24,32 +24,35 @@ function save_request(request_id, request, session_id) {
24
24
  }
25
25
  request.session_id = session_id;
26
26
  request.initiator = initiator;
27
-
27
+
28
28
  if (request.in_progress) {
29
- request.status_code_view = '<img class="icon" src="static/icons/wait-sandclock-icon.svg-+-$**HTTPDBG_VERSION**$" alt="loading"/>';
29
+ request.status_code_view = '<svg class="icon" alt="loading"><use href="#wait-sandclock-icon"></use></svg>';
30
30
  } else {
31
31
  switch (request.status_code) {
32
32
  case 0:
33
- request.status_code_view = '<img class="icon" src="static/icons/wait-sandclock-icon.svg-+-$**HTTPDBG_VERSION**$/" alt="loading"/>';
33
+ request.status_code_view = '<svg class="icon" alt="loading"><use href="#wait-sandclock-icon"></use></svg>';
34
34
  break;
35
35
  case -1:
36
- request.status_code_view = '<img class="icon" src="static/icons/math-multiplication-icon.svg-+-$**HTTPDBG_VERSION**$/" alt="load failed"/>';
36
+ request.status_code_view = '<svg class="icon" alt="load failed"><use href="#math-multiplication-icon"></use></svg>';
37
37
  break;
38
38
  default:
39
39
  request.status_code_view = request.status_code;
40
40
  break;
41
41
  }
42
42
  }
43
-
44
- global.requests[request_id] = request;
45
-
43
+
44
+ global.requests[request_id] = request;
45
+
46
46
  if (!request.pin) {
47
47
  get_request(request_id);
48
- }
48
+ }
49
49
  }
50
50
  }
51
51
 
52
- async function get_all_requests() {
52
+ async function load_all_requests() {
53
+ if (typeof global.static_all_requests !== "undefined") {
54
+ return global.static_all_requests;
55
+ }
53
56
 
54
57
  var requests_already_loaded = 0
55
58
  for (const [request_id, request] of Object.entries(global.requests)) {
@@ -62,15 +65,22 @@ async function get_all_requests() {
62
65
  "requests_already_loaded": requests_already_loaded,
63
66
  })
64
67
 
65
- await fetch(url)
66
- .then(res => res.json())
68
+ const res = await fetch(url);
69
+ const data = await res.json();
70
+ return data;
71
+ }
72
+
73
+
74
+ async function get_all_requests() {
75
+
76
+ await load_all_requests()
67
77
  .then(data => {
68
78
  global.connected = true;
69
79
 
70
80
  if (data.session.id != global.session) {
71
- clean();
81
+ clean();
72
82
  global.session = data.session.id;
73
- global.sessions[data.session.id] = data.session;
83
+ global.sessions[data.session.id] = data.session;
74
84
  };
75
85
 
76
86
  // for the initiators and the groups, we can just save them without any verification
@@ -93,13 +103,21 @@ async function get_all_requests() {
93
103
  .catch((error) => {
94
104
  global.connected = false;
95
105
  });
106
+ }
107
+
108
+ async function load_request(request_id) {
109
+ if (typeof global.static_requests !== "undefined") {
110
+ return global.static_requests[request_id];
111
+ }
96
112
 
113
+ const res = await fetch("/request/" + request_id);
114
+ const data = await res.json();
115
+ return data;
97
116
  }
98
117
 
99
118
  async function get_request(request_id) {
100
119
 
101
- await fetch("/request/" + request_id)
102
- .then(res => res.json())
120
+ await load_request(request_id)
103
121
  .then(data => {
104
122
  global.connected = true;
105
123
  global.requests[request_id].filter = prepare_for_filter(global.requests[request_id].url);
@@ -6,25 +6,25 @@
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
7
  <title>httpdbg</title>
8
8
 
9
- <link rel="shortcut icon" href="static/favicon.ico-+-$**HTTPDBG_VERSION**$">
9
+ <link rel="shortcut icon" href="static/favicon.ico">
10
10
 
11
11
  $**PRELOAD_ICONS**$
12
12
 
13
- <link rel="stylesheet" href="static/style.css-+-$**HTTPDBG_VERSION**$">
14
- <link rel="stylesheet" href="static/table.css-+-$**HTTPDBG_VERSION**$">
15
- <link rel="stylesheet" href="static/column.css-+-$**HTTPDBG_VERSION**$">
16
- <link rel="stylesheet" href="static/detail.css-+-$**HTTPDBG_VERSION**$">
17
- <link rel="stylesheet" href="static/configuration.css-+-$**HTTPDBG_VERSION**$">
13
+ <link rel="stylesheet" href="static/style.css">
14
+ <link rel="stylesheet" href="static/table.css">
15
+ <link rel="stylesheet" href="static/column.css">
16
+ <link rel="stylesheet" href="static/detail.css">
17
+ <link rel="stylesheet" href="static/configuration.css">
18
18
  <style type="text/css" id="filter"></style>
19
19
  <style type="text/css" id="configurationcss"></style>
20
20
  <style type="text/css" id="serverstatuscss"></style>
21
21
  <style type="text/css" id="collapsegroupcss"></style>
22
- <script src="static/mustache.min.js-+-$**HTTPDBG_VERSION**$"></script>
23
- <script src="static/main.js-+-$**HTTPDBG_VERSION**$"></script>
24
- <script src="static/api.js-+-$**HTTPDBG_VERSION**$"></script>
25
- <script src="static/render.js-+-$**HTTPDBG_VERSION**$"></script>
26
- <script src="static/filter.js-+-$**HTTPDBG_VERSION**$"></script>
27
- <script src="static/configuration.js-+-$**HTTPDBG_VERSION**$"></script>
22
+ <script src="static/mustache.min.js"></script>
23
+ <script src="static/main.js"></script>
24
+ <script src="static/api.js"></script>
25
+ <script src="static/render.js"></script>
26
+ <script src="static/filter.js"></script>
27
+ <script src="static/configuration.js"></script>
28
28
  </head>
29
29
 
30
30
  <body>
@@ -32,15 +32,16 @@
32
32
  <nav id="navbar">
33
33
  <div class="menu-left">
34
34
  <button id="trash" type="button" onclick="clean(true)" title="Delete all unpinned requests.">
35
- <img class="icon" src="static/icons/recycle-bin-line-icon.svg-+-$**HTTPDBG_VERSION**$"
36
- alt="Delete all unpinned requests." />
35
+ <svg class="icon">
36
+ <use href="#recycle-bin-line-icon"></use>
37
+ </svg>
37
38
  </button>
38
39
  <form id="configform" onsubmit="return false;">
39
40
  -
40
41
  <input type="checkbox" id="ckeepsession" name="kp" onchange="load_config_from_form(true)">
41
42
  <label for="ckeepsession"
42
- title="If you choose to keep previous sessions, the requests will not be deleted when starting a new session.">
43
- keep previous sessions
43
+ title="If you choose to keep hisotry, the requests will not be deleted when starting a new session.">
44
+ keep history
44
45
  </label>
45
46
  -
46
47
  <label for="groupby-select">group by</label>
@@ -55,40 +56,48 @@
55
56
  onchange="load_config_from_form(true)">
56
57
  <label for="cinitiator"
57
58
  title="Hide the groups.&#10;Be aware that the order of the requests will not be affected.">
58
- <img class="icon config-checkbox-unchecked"
59
- src="static/icons/eye-icon.svg-+-$**HTTPDBG_VERSION**$" />
60
- <img class="icon config-checkbox-checked"
61
- src="static/icons/eye-blind-icon.svg-+-$**HTTPDBG_VERSION**$" />
59
+ <svg class="icon config-checkbox-unchecked">
60
+ <use href="#eye-icon"></use>
61
+ </svg>
62
+ <svg class="icon config-checkbox-checked">
63
+ <use href="#eye-blind-icon"></use>
64
+ </svg>
62
65
  group
63
66
  </label>
64
67
  -
65
68
  <input class="config-checkbox" type="checkbox" id="ctag" name="ht"
66
69
  onchange="load_config_from_form(true)">
67
70
  <label for="ctag" title="Hide the tags.">
68
- <img class="icon config-checkbox-unchecked"
69
- src="static/icons/eye-icon.svg-+-$**HTTPDBG_VERSION**$" />
70
- <img class="icon config-checkbox-checked"
71
- src="static/icons/eye-blind-icon.svg-+-$**HTTPDBG_VERSION**$" />
71
+ <svg class="icon config-checkbox-unchecked">
72
+ <use href="#eye-icon"></use>
73
+ </svg>
74
+ <svg class="icon config-checkbox-checked">
75
+ <use href="#eye-blind-icon"></use>
76
+ </svg>
72
77
  tag
73
78
  </label>
74
79
  -
75
80
  <input class="config-checkbox" type="checkbox" id="curl" name="hn"
76
81
  onchange="load_config_from_form(true)">
77
82
  <label for="curl" title="Hide the network location and the scheme in the url.">
78
- <img class="icon config-checkbox-unchecked"
79
- src="static/icons/eye-icon.svg-+-$**HTTPDBG_VERSION**$" />
80
- <img class="icon config-checkbox-checked"
81
- src="static/icons/eye-blind-icon.svg-+-$**HTTPDBG_VERSION**$" />
83
+ <svg class="icon config-checkbox-unchecked">
84
+ <use href="#eye-icon"></use>
85
+ </svg>
86
+ <svg class="icon config-checkbox-checked">
87
+ <use href="#eye-blind-icon"></use>
88
+ </svg>
82
89
  netloc
83
90
  </label>
84
91
  -
85
92
  <input class="config-checkbox" type="checkbox" id="cwrap" name="wl"
86
93
  onchange="load_config_from_form(true)">
87
94
  <label for="cwrap" title="Wrap lines on the request/response payload content.">
88
- <img class="icon config-checkbox-unchecked"
89
- src="static/icons/lock-open-line-icon.svg-+-$**HTTPDBG_VERSION**$" />
90
- <img class="icon config-checkbox-checked"
91
- src="static/icons/lock-line-icon.svg-+-$**HTTPDBG_VERSION**$" />
95
+ <svg class="icon config-checkbox-unchecked">
96
+ <use href="#lock-open-line-icon"></use>
97
+ </svg>
98
+ <svg class="icon config-checkbox-checked">
99
+ <use href="#lock-line-icon"></use>
100
+ </svg>
92
101
  wrap lines
93
102
  </label>
94
103
  -
@@ -96,10 +105,12 @@
96
105
  onchange="load_config_from_form(true)">
97
106
  <label for="craw"
98
107
  title="Avoid formatting the request/response payload content according to its content type.">
99
- <img class="icon config-checkbox-unchecked"
100
- src="static/icons/lock-open-line-icon.svg-+-$**HTTPDBG_VERSION**$" />
101
- <img class="icon config-checkbox-checked"
102
- src="static/icons/lock-line-icon.svg-+-$**HTTPDBG_VERSION**$" />
108
+ <svg class="icon config-checkbox-unchecked">
109
+ <use href="#lock-open-line-icon"></use>
110
+ </svg>
111
+ <svg class="icon config-checkbox-checked">
112
+ <use href="#lock-line-icon"></use>
113
+ </svg>
103
114
  raw data
104
115
  </label>
105
116
  </form>
@@ -107,8 +118,9 @@
107
118
  <div class="menu-right">
108
119
  <span title="httpdbg v$**HTTPDBG_VERSION**$">httpdbg</span>
109
120
  <button onclick="help()" title="open help">
110
- <img class="icon" src="static/icons/question-mark-round-line-icon.svg-+-$**HTTPDBG_VERSION**$"
111
- alt="open help" />
121
+ <svg class="icon">
122
+ <use href="#question-mark-round-line-icon"></use>
123
+ </svg>
112
124
  </button>
113
125
  </div>
114
126
  </nav>
@@ -165,9 +177,9 @@
165
177
  <th class="url">
166
178
  url
167
179
  &nbsp;
168
- <img class="icon icon-filter"
169
- src="static/icons/filter-outline-icon.svg-+-$**HTTPDBG_VERSION**$"
170
- alt="filter for url" />
180
+ <svg class="icon icon-filter">
181
+ <use href="#filter-outline-icon"></use>
182
+ </svg>
171
183
  &nbsp;
172
184
  <label for="filter-url" class="visuallyhidden">
173
185
  filter for url
@@ -186,7 +198,7 @@
186
198
  <tr class="group" data-orderby="{{tbegin}}">
187
199
  <td class="collapse" onclick="event.stopPropagation();">
188
200
  <input class="collapse-checkbox" type="checkbox" id="collapse{{id}}" onclick="collapse_group('{{id}}',this.checked)">
189
- <label class="collapse-icon" for="collapse{{id}}"><img class="icon" src="static/icons/chevron-direction-top-outline-icon.svg-+-$**HTTPDBG_VERSION**$" alt="click to collapse/expand"/></label>
201
+ <label class="collapse-icon" for="collapse{{id}}"><svg class="icon"><use href="#chevron-direction-top-outline-icon"></use></svg></label>
190
202
  </td>
191
203
  <td colspan="3" class="group-label" title="{{full_label}}">
192
204
  {{label}}
@@ -207,7 +219,7 @@
207
219
  data-qa-request="{{id}}">
208
220
  <td class="pin" onclick="event.stopPropagation();">
209
221
  <input class="pin-checkbox" type="checkbox" id="pin{{id}}" onclick="pin_request('{{id}}',this.checked)" {{pin}}>
210
- <label class="pin-icon" for="pin{{id}}"><img class="icon" src="static/icons/push-pin-green-icon.svg-+-$**HTTPDBG_VERSION**$" alt="click to pin"/></label>
222
+ <label class="pin-icon" for="pin{{id}}"><svg class="icon"><use href="#push-pin-green-icon.svg"></use></svg></label>
211
223
  </td>
212
224
  <td class="status"><span title="{{reason}}" data-qa-request-status>{{{status_code_view}}}</span></td>
213
225
  <td class="method"><span title="{{verb}}" data-qa-request-method>{{verb}}</span></td>
@@ -348,14 +360,14 @@
348
360
 
349
361
  {{#body}}
350
362
  {{#body.path}}
351
- <a href="{{body.path}}" download="{{body.filename}}" class="need-server"><button title="download"><img class="icon" src="static/icons/download-install-line-icon.svg-+-$**HTTPDBG_VERSION**$"/></button></a>
352
- <img class="icon need-server-info" title="Use the '-k' option to keep the server up and being able to download the content." src="static/icons/information-mark-circle-outline-icon.svg-+-$**HTTPDBG_VERSION**$" />
363
+ <a href="{{body.path}}" download="{{body.filename}}" class="need-server"><button title="download"><svg class="icon"><use href="#download-install-line-icon"></use></svg></button></a>
364
+ <svg class="icon need-server-info"><use href="#information-mark-circle-outline-icon"></use></svg>
353
365
  {{/body.path}}
354
366
 
355
367
  {{#body.text}}
356
368
 
357
369
  {{#body.raw_text}}
358
- <button title="copy to clipboard" onclick="navigator.clipboard.writeText({{body.raw_text}});"><img class="icon" src="static/icons/files-papers-outline-icon.svg-+-$**HTTPDBG_VERSION**$"/></button>
370
+ <button title="copy to clipboard" onclick="navigator.clipboard.writeText({{body.raw_text}});"><svg class="icon"><use href="#files-papers-outline-icon"></use></svg></button>
359
371
  {{/body.raw_text}}
360
372
 
361
373
  <label><input type="checkbox" name="ckwraptext" onclick='show_wrap_lines(this.parentNode.parentNode.querySelector(".previewcode"));'>Wrap lines</label>
@@ -418,6 +430,7 @@
418
430
  </table>
419
431
  </div>
420
432
  </div>
433
+ $**EXPORT**$
421
434
  <script>
422
435
  load_config_from_url(true);
423
436
  update_filter_url();
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: httpdbg
3
- Version: 2.0.2
3
+ Version: 2.1.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
@@ -192,6 +192,8 @@ options:
192
192
  --only-client record only HTTP client requests
193
193
  --keep-up, -k keep the server up even if the requests have been read
194
194
  --force-quit, -q stop the server even if the requests have not been read
195
+ --export-html EXPORT_HTML
196
+ the path to the export file
195
197
  --console run a python console (default)
196
198
  --module MODULE, -m MODULE
197
199
  run library module as a script (the next args are passed to pytest as is)
@@ -225,6 +227,10 @@ The requests:
225
227
  * are still available in the web page even if the python process stopped (except if you force quit before the requests have been loaded by the web page).
226
228
  * are automatically cleaned if a new execution is detected.
227
229
 
230
+ ## export
231
+
232
+ You can export the HTTP traces in a single HTML file. In that case, the web interface is not available during the execution of the python command.
233
+
228
234
  ## limitations
229
235
 
230
236
  Theoretically, if your HTTP client or server uses a standard Python socket, the HTTP requests will be recorded.
@@ -7,6 +7,7 @@ httpdbg/__main__.py
7
7
  httpdbg/args.py
8
8
  httpdbg/env.py
9
9
  httpdbg/exception.py
10
+ httpdbg/export.py
10
11
  httpdbg/http_status_code.py
11
12
  httpdbg/initiator.py
12
13
  httpdbg/log.py
@@ -1,7 +0,0 @@
1
- from httpdbg.hooks.all import httprecord
2
- from httpdbg.records import HTTPRecords
3
-
4
-
5
- __version__ = "2.0.2"
6
-
7
- __all__ = ["httprecord", "HTTPRecords"]
@@ -1,69 +0,0 @@
1
- import sys
2
- import time
3
-
4
- from httpdbg import __version__
5
- from httpdbg.args import read_args
6
- from httpdbg.hooks.all import httprecord
7
- from httpdbg.log import set_env_for_logging
8
- from httpdbg.server import httpdbg_srv
9
- from httpdbg.mode_console import run_console
10
- from httpdbg.mode_module import run_module
11
- from httpdbg.mode_script import run_script
12
-
13
-
14
- def print_msg(msg):
15
- sep = ".... - - .--. -.. -... --. .... - - .--. -.. -... --. .... - - .--. -.. -... --."
16
- msg = f"{sep}\n{msg}\n{sep}"
17
- print(msg)
18
-
19
-
20
- def pyhttpdbg(params, subparams, test_mode=False):
21
-
22
- set_env_for_logging(params.log_level, params.log)
23
-
24
- url = f"http://{params.host}:{params.port}/{'?hi=on' if params.console else ''}"
25
-
26
- if not params.no_banner:
27
- print_msg(f" httpdbg - HTTP(S) requests available at {url}")
28
-
29
- sys.path.insert(0, "") # to mimic the default python command behavior
30
-
31
- with httpdbg_srv(params.host, params.port) as records:
32
- records.server = not params.only_client
33
- with httprecord(records, params.initiator, server=records.server):
34
- if params.module:
35
- run_module(subparams)
36
- elif params.script:
37
- run_script(subparams)
38
- else:
39
- run_console(records, test_mode)
40
-
41
- if not (params.force_quit or test_mode):
42
- if not params.no_banner:
43
- print_msg(f" httpdbg - HTTP(S) requests available at {url}")
44
-
45
- if params.keep_up:
46
- input("Press enter to quit")
47
- else:
48
- # we keep the server up until all the requests have been loaded in the web interface
49
- print(
50
- "Waiting until all the requests have been loaded in the web interface."
51
- )
52
- print("Press Ctrl+C to quit.")
53
- try:
54
- while records.unread:
55
- time.sleep(0.5)
56
- except KeyboardInterrupt: # pragma: no cover
57
- pass
58
-
59
-
60
- def pyhttpdbg_entry_point(test_mode=False):
61
- params, subparams = read_args(sys.argv[1:])
62
- if params.version:
63
- print(__version__)
64
- else:
65
- pyhttpdbg(params, subparams, test_mode=test_mode)
66
-
67
-
68
- if __name__ == "__main__":
69
- pyhttpdbg_entry_point()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes