pypproxy 0.2.0__tar.gz → 0.3.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 (139) hide show
  1. {pypproxy-0.2.0 → pypproxy-0.3.0}/PKG-INFO +1 -1
  2. pypproxy-0.3.0/pypproxy/ui/app.py +582 -0
  3. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/ui/detail.py +41 -28
  4. pypproxy-0.3.0/pypproxy/ui/theme.py +375 -0
  5. {pypproxy-0.2.0 → pypproxy-0.3.0}/pyproject.toml +1 -1
  6. {pypproxy-0.2.0 → pypproxy-0.3.0}/uv.lock +1 -1
  7. pypproxy-0.2.0/pypproxy/ui/app.py +0 -473
  8. pypproxy-0.2.0/pypproxy/ui/theme.py +0 -59
  9. {pypproxy-0.2.0 → pypproxy-0.3.0}/.github/dependabot.yml +0 -0
  10. {pypproxy-0.2.0 → pypproxy-0.3.0}/.github/workflows/ci.yml +0 -0
  11. {pypproxy-0.2.0 → pypproxy-0.3.0}/.github/workflows/docs.yml +0 -0
  12. {pypproxy-0.2.0 → pypproxy-0.3.0}/.github/workflows/publish.yml +0 -0
  13. {pypproxy-0.2.0 → pypproxy-0.3.0}/.gitignore +0 -0
  14. {pypproxy-0.2.0 → pypproxy-0.3.0}/.pre-commit-config.yaml +0 -0
  15. {pypproxy-0.2.0 → pypproxy-0.3.0}/LICENSE +0 -0
  16. {pypproxy-0.2.0 → pypproxy-0.3.0}/Makefile +0 -0
  17. {pypproxy-0.2.0 → pypproxy-0.3.0}/README.md +0 -0
  18. {pypproxy-0.2.0 → pypproxy-0.3.0}/docs/api.md +0 -0
  19. {pypproxy-0.2.0 → pypproxy-0.3.0}/docs/architecture.md +0 -0
  20. {pypproxy-0.2.0 → pypproxy-0.3.0}/docs/configuration.md +0 -0
  21. {pypproxy-0.2.0 → pypproxy-0.3.0}/docs/getting-started.md +0 -0
  22. {pypproxy-0.2.0 → pypproxy-0.3.0}/docs/index.md +0 -0
  23. {pypproxy-0.2.0 → pypproxy-0.3.0}/docs/protocols.md +0 -0
  24. {pypproxy-0.2.0 → pypproxy-0.3.0}/docs/replay.md +0 -0
  25. {pypproxy-0.2.0 → pypproxy-0.3.0}/docs/rule-engine.md +0 -0
  26. {pypproxy-0.2.0 → pypproxy-0.3.0}/docs/scripting.md +0 -0
  27. {pypproxy-0.2.0 → pypproxy-0.3.0}/docs/web-ui.md +0 -0
  28. {pypproxy-0.2.0 → pypproxy-0.3.0}/main.py +0 -0
  29. {pypproxy-0.2.0 → pypproxy-0.3.0}/mkdocs.yml +0 -0
  30. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/__init__.py +0 -0
  31. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/ab_test/__init__.py +0 -0
  32. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/ab_test/runner.py +0 -0
  33. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/analytics/__init__.py +0 -0
  34. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/analytics/stats.py +0 -0
  35. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/api/__init__.py +0 -0
  36. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/api/server.py +0 -0
  37. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/bulk/__init__.py +0 -0
  38. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/bulk/sender.py +0 -0
  39. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/cert/__init__.py +0 -0
  40. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/cert/ca.py +0 -0
  41. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/cert/client_cert.py +0 -0
  42. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/codec.py +0 -0
  43. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/codegen/__init__.py +0 -0
  44. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/codegen/generator.py +0 -0
  45. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/config/__init__.py +0 -0
  46. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/config/config.py +0 -0
  47. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/dns/__init__.py +0 -0
  48. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/dns/server.py +0 -0
  49. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/exporter/__init__.py +0 -0
  50. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/exporter/exporter.py +0 -0
  51. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/exporter/importer.py +0 -0
  52. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/frida/__init__.py +0 -0
  53. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/frida/device.py +0 -0
  54. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/frida/hook_generator.py +0 -0
  55. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/frida/pinning_bypass.py +0 -0
  56. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/graphql/__init__.py +0 -0
  57. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/graphql/detector.py +0 -0
  58. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/graphql/introspection.py +0 -0
  59. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/graphql/modifier.py +0 -0
  60. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/graphql/schema_store.py +0 -0
  61. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/intercept/__init__.py +0 -0
  62. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/intercept/manager.py +0 -0
  63. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/interceptor/__init__.py +0 -0
  64. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/interceptor/interceptor.py +0 -0
  65. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/macro/__init__.py +0 -0
  66. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/macro/runner.py +0 -0
  67. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/openapi/__init__.py +0 -0
  68. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/openapi/generator.py +0 -0
  69. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/proto/__init__.py +0 -0
  70. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/proto/grpc.py +0 -0
  71. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/proto/mqtt.py +0 -0
  72. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/proto/ws.py +0 -0
  73. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/proto/ws_intercept.py +0 -0
  74. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/proxy/__init__.py +0 -0
  75. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/proxy/proxy.py +0 -0
  76. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/replay/__init__.py +0 -0
  77. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/replay/replay.py +0 -0
  78. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/report/__init__.py +0 -0
  79. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/report/generator.py +0 -0
  80. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/rule/__init__.py +0 -0
  81. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/rule/rule.py +0 -0
  82. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/scan/__init__.py +0 -0
  83. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/scan/scanner.py +0 -0
  84. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/script/__init__.py +0 -0
  85. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/script/engine.py +0 -0
  86. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/security/__init__.py +0 -0
  87. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/security/advanced_checks.py +0 -0
  88. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/security/header_checker.py +0 -0
  89. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/security/idor.py +0 -0
  90. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/security/int_overflow.py +0 -0
  91. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/security/jwt_checker.py +0 -0
  92. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/security/plugin.py +0 -0
  93. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/security/randomness.py +0 -0
  94. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/session/__init__.py +0 -0
  95. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/session/manager.py +0 -0
  96. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/store/__init__.py +0 -0
  97. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/store/db.py +0 -0
  98. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/store/filter_parser.py +0 -0
  99. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/store/fts.py +0 -0
  100. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/store/models.py +0 -0
  101. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/store/scope.py +0 -0
  102. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/store/store.py +0 -0
  103. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/ui/__init__.py +0 -0
  104. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/ui/ab_tab.py +0 -0
  105. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/ui/advanced_security_tab.py +0 -0
  106. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/ui/analytics_tab.py +0 -0
  107. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/ui/bulk_sender_ui.py +0 -0
  108. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/ui/codegen_tab.py +0 -0
  109. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/ui/cui.py +0 -0
  110. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/ui/diff_view.py +0 -0
  111. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/ui/frida_tab.py +0 -0
  112. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/ui/graphql_tab.py +0 -0
  113. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/ui/import_tab.py +0 -0
  114. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/ui/intercept_dialog.py +0 -0
  115. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/ui/macro_tab.py +0 -0
  116. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/ui/openapi_tab.py +0 -0
  117. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/ui/report_tab.py +0 -0
  118. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/ui/resender.py +0 -0
  119. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/ui/scan_tab.py +0 -0
  120. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/ui/security_tab.py +0 -0
  121. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/ui/session_tab.py +0 -0
  122. {pypproxy-0.2.0 → pypproxy-0.3.0}/pypproxy/ui/settings.py +0 -0
  123. {pypproxy-0.2.0 → pypproxy-0.3.0}/tests/__init__.py +0 -0
  124. {pypproxy-0.2.0 → pypproxy-0.3.0}/tests/test_advanced.py +0 -0
  125. {pypproxy-0.2.0 → pypproxy-0.3.0}/tests/test_advanced_tools.py +0 -0
  126. {pypproxy-0.2.0 → pypproxy-0.3.0}/tests/test_all_features.py +0 -0
  127. {pypproxy-0.2.0 → pypproxy-0.3.0}/tests/test_bulk.py +0 -0
  128. {pypproxy-0.2.0 → pypproxy-0.3.0}/tests/test_cert.py +0 -0
  129. {pypproxy-0.2.0 → pypproxy-0.3.0}/tests/test_codec.py +0 -0
  130. {pypproxy-0.2.0 → pypproxy-0.3.0}/tests/test_decode.py +0 -0
  131. {pypproxy-0.2.0 → pypproxy-0.3.0}/tests/test_exporter.py +0 -0
  132. {pypproxy-0.2.0 → pypproxy-0.3.0}/tests/test_filter_parser.py +0 -0
  133. {pypproxy-0.2.0 → pypproxy-0.3.0}/tests/test_frida.py +0 -0
  134. {pypproxy-0.2.0 → pypproxy-0.3.0}/tests/test_graphql.py +0 -0
  135. {pypproxy-0.2.0 → pypproxy-0.3.0}/tests/test_interceptor.py +0 -0
  136. {pypproxy-0.2.0 → pypproxy-0.3.0}/tests/test_mqtt.py +0 -0
  137. {pypproxy-0.2.0 → pypproxy-0.3.0}/tests/test_rule.py +0 -0
  138. {pypproxy-0.2.0 → pypproxy-0.3.0}/tests/test_security.py +0 -0
  139. {pypproxy-0.2.0 → pypproxy-0.3.0}/tests/test_store.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypproxy
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: MITM HTTP/HTTPS proxy for inspecting and modifying traffic
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.11
@@ -0,0 +1,582 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+
5
+ from nicegui import app as nicegui_app
6
+ from nicegui import ui
7
+
8
+ from pypproxy.intercept.manager import InterceptManager
9
+ from pypproxy.store.models import Entry, Filter
10
+ from pypproxy.store.store import Store
11
+
12
+ from .intercept_dialog import build_intercept_panel
13
+ from .theme import PALETTE, apply_dark_theme
14
+
15
+ _ROW_COLORS = ["", "#b91c1c", "#166534", "#1e40af", "#b45309", "#6b21a8"]
16
+ _COLOR_LABELS = ["None", "Red", "Green", "Blue", "Yellow", "Purple"]
17
+
18
+ # Navigation structure
19
+ _NAV = [
20
+ ("Traffic", [("Traffic", "list", "traffic")]),
21
+ (
22
+ "Tools",
23
+ [
24
+ ("Resender", "send", "resender"),
25
+ ("Bulk Sender", "dynamic_feed", "bulk"),
26
+ ("Macro", "playlist_play", "macro"),
27
+ ("Diff", "difference", "diff"),
28
+ ("A/B Test", "compare", "ab"),
29
+ ],
30
+ ),
31
+ (
32
+ "Security",
33
+ [
34
+ ("Security", "security", "security"),
35
+ ("Adv Security", "shield", "advsec"),
36
+ ("Scan", "search", "scan"),
37
+ ("CORS/SSRF", "bug_report", "advsec2"),
38
+ ],
39
+ ),
40
+ (
41
+ "Analysis",
42
+ [
43
+ ("GraphQL", "account_tree", "graphql"),
44
+ ("Analytics", "bar_chart", "analytics"),
45
+ ("OpenAPI", "description", "openapi"),
46
+ ],
47
+ ),
48
+ (
49
+ "Dev",
50
+ [
51
+ ("Code Gen", "code", "codegen"),
52
+ ("Frida", "adb", "frida"),
53
+ ("Sessions", "folder", "sessions"),
54
+ ("Report", "summarize", "report"),
55
+ ],
56
+ ),
57
+ (
58
+ "Data",
59
+ [
60
+ ("Import/Search", "upload", "import"),
61
+ ],
62
+ ),
63
+ ]
64
+
65
+
66
+ def build_ui(
67
+ store: Store,
68
+ intercept_mgr: InterceptManager | None = None,
69
+ settings_kwargs: dict | None = None,
70
+ ) -> None:
71
+ if settings_kwargs:
72
+ from .settings import build_settings_page
73
+
74
+ build_settings_page(**settings_kwargs)
75
+
76
+ @ui.page("/")
77
+ async def index() -> None:
78
+ apply_dark_theme()
79
+ ui.dark_mode().enable()
80
+
81
+ state: dict = {
82
+ "entries": [],
83
+ "selected": None,
84
+ "filter": Filter(),
85
+ "compare_left": None,
86
+ "active_page": "traffic",
87
+ }
88
+
89
+ # ── Root layout: sidebar + main ──────────────────────────
90
+ with ui.row().classes("w-full").style("height:100vh; overflow:hidden; gap:0"):
91
+ # ── Sidebar ──────────────────────────────────────────
92
+ with ui.element("div").classes("pp-sidebar"):
93
+ with ui.element("div").classes("pp-logo"):
94
+ ui.element("div").classes("pp-logo-name").text = "pypproxy"
95
+ with ui.row().classes("items-center gap-2").style("margin-top:4px"):
96
+ ui.element("div").classes("pp-status-dot").tooltip("Proxy running")
97
+ ui.element("div").classes("pp-logo-sub").text = "v0.2.0 · :8080"
98
+
99
+ nav_items: dict[str, ui.element] = {}
100
+
101
+ for group_name, items in _NAV:
102
+ with ui.element("div").classes("pp-nav-section"):
103
+ ui.element("div").classes("pp-nav-section-label").text = group_name
104
+ for label, icon, page_id in items:
105
+ item_el = ui.element("div").classes("pp-nav-item")
106
+ with item_el:
107
+ ui.html(f'<span class="material-icons pp-nav-icon">{icon}</span>')
108
+ ui.label(label).style("font-size:13.5px")
109
+
110
+ def _nav(pid=page_id, el=item_el) -> None:
111
+ state["active_page"] = pid
112
+ for _, v in nav_items.items():
113
+ v.classes(remove="active")
114
+ el.classes("active")
115
+ page_container.clear()
116
+ _render_page(
117
+ pid, store, state, intercept_mgr, page_container, nav_items
118
+ )
119
+
120
+ item_el.on("click", _nav)
121
+ nav_items[page_id] = item_el
122
+
123
+ # Settings link at bottom
124
+ ui.element("div").style("flex:1")
125
+ with ui.element("div").style(
126
+ "padding:12px 18px 16px; border-top:1px solid var(--pp-border)"
127
+ ):
128
+ settings_item = ui.element("div").classes("pp-nav-item").style("padding:6px 0")
129
+ with settings_item:
130
+ ui.html(
131
+ '<span class="material-icons pp-nav-icon" style="font-size:14px">settings</span>'
132
+ )
133
+ ui.label("Settings").style("font-size:12px")
134
+ settings_item.on("click", lambda: ui.navigate.to("/settings"))
135
+
136
+ # ── Main area ─────────────────────────────────────────
137
+ with ui.element("div").classes("pp-main"):
138
+ # Toolbar
139
+ with ui.element("div").classes("pp-toolbar"):
140
+ filter_input = (
141
+ ui.input(placeholder="host == example.com && method == POST")
142
+ .props("dense outlined dark")
143
+ .classes("pp-filter-wrap")
144
+ .tooltip(
145
+ "Fields: host path method status protocol request response full_text | Ops: == != contains ~ | Logic: && ||"
146
+ )
147
+ )
148
+
149
+ ui.element("div").style("flex:1")
150
+
151
+ if intercept_mgr is not None:
152
+ intercept_toggle = (
153
+ ui.switch("Intercept")
154
+ .props("dense dark color=warning")
155
+ .tooltip("Pause requests")
156
+ )
157
+ intercept_toggle.on(
158
+ "update:model-value",
159
+ lambda e: intercept_mgr.set_enabled(e.args),
160
+ )
161
+
162
+ ui.button(
163
+ icon="delete_sweep", on_click=lambda: _clear_traffic(store, state)
164
+ ).props("flat dense size=sm color=negative").tooltip("Clear traffic")
165
+ ui.button(
166
+ icon="light_mode",
167
+ on_click=lambda: ui.run_javascript(
168
+ "const t = ppToggleTheme(); "
169
+ "document.querySelector('.pp-theme-btn .material-icons').textContent = "
170
+ "t === 'light' ? 'dark_mode' : 'light_mode';"
171
+ ),
172
+ ).props("flat dense size=sm").classes("pp-theme-btn").style(
173
+ f"color:{PALETTE['text_muted']}"
174
+ ).tooltip("Toggle light/dark mode")
175
+ ui.button(icon="settings", on_click=lambda: ui.navigate.to("/settings")).props(
176
+ "flat dense size=sm"
177
+ ).style(f"color:{PALETTE['text_muted']}").tooltip("Settings")
178
+
179
+ # Page container
180
+ page_container = ui.element("div").style(
181
+ "flex:1; overflow:hidden; display:flex; flex-direction:column"
182
+ )
183
+
184
+ # Filter reactivity
185
+ def apply_filter() -> None:
186
+ state["filter"] = Filter(expression=filter_input.value or "")
187
+ if state["active_page"] == "traffic":
188
+ _render_page(
189
+ "traffic", store, state, intercept_mgr, page_container, nav_items
190
+ )
191
+
192
+ filter_input.on("update:model-value", lambda: apply_filter())
193
+
194
+ if intercept_mgr is not None:
195
+ build_intercept_panel(intercept_mgr, page_container)
196
+
197
+ # Activate traffic page by default
198
+ nav_items["traffic"].classes("active")
199
+ _render_page("traffic", store, state, intercept_mgr, page_container, nav_items)
200
+
201
+ # Live update poller
202
+ q = store.subscribe()
203
+
204
+ async def _poller() -> None:
205
+ try:
206
+ while True:
207
+ try:
208
+ entry = q.get_nowait()
209
+ if state["active_page"] == "traffic" and state["filter"].matches(entry):
210
+ existing = next(
211
+ (i for i, e in enumerate(state["entries"]) if e.id == entry.id),
212
+ None,
213
+ )
214
+ if existing is not None:
215
+ state["entries"][existing] = entry
216
+ else:
217
+ state["entries"].insert(0, entry)
218
+ if "traffic_table" in state:
219
+ _update_table(state["entries"], state["traffic_table"])
220
+ except asyncio.QueueEmpty:
221
+ pass
222
+ await asyncio.sleep(0.2)
223
+ except asyncio.CancelledError:
224
+ store.unsubscribe(q)
225
+
226
+ nicegui_app.on_shutdown(lambda: store.unsubscribe(q))
227
+ asyncio.ensure_future(_poller())
228
+
229
+
230
+ def _render_page(
231
+ page_id: str,
232
+ store: Store,
233
+ state: dict,
234
+ intercept_mgr,
235
+ container: ui.element,
236
+ nav_items: dict,
237
+ ) -> None:
238
+ container.clear()
239
+ with container:
240
+ if page_id == "traffic":
241
+ _build_traffic_page(store, state, intercept_mgr)
242
+ elif page_id == "resender":
243
+ from .resender import build_resender_tab
244
+
245
+ build_resender_tab(store)
246
+ elif page_id == "bulk":
247
+ from .bulk_sender_ui import build_bulk_sender
248
+
249
+ bulk_state = build_bulk_sender(
250
+ ui.column().classes("w-full h-full overflow-auto q-pa-md")
251
+ )
252
+ state["bulk_state"] = bulk_state
253
+ elif page_id == "macro":
254
+ from .macro_tab import build_macro_tab
255
+
256
+ macro_state = build_macro_tab(store)
257
+ state["macro_state"] = macro_state
258
+ elif page_id == "diff":
259
+ from .diff_view import build_diff_view
260
+
261
+ diff_state = build_diff_view(ui.column().classes("w-full h-full overflow-auto q-pa-md"))
262
+ state["diff_state"] = diff_state
263
+ elif page_id == "ab":
264
+ from .ab_tab import build_ab_tab
265
+
266
+ ab_state = build_ab_tab(store)
267
+ state["ab_state"] = ab_state
268
+ elif page_id == "security":
269
+ from .security_tab import build_security_tab
270
+
271
+ sec_state = build_security_tab(store)
272
+ state["sec_state"] = sec_state
273
+ elif page_id == "advsec":
274
+ from .advanced_security_tab import build_advanced_security_tab
275
+
276
+ advsec_state = build_advanced_security_tab(store)
277
+ state["advsec_state"] = advsec_state
278
+ elif page_id == "scan":
279
+ from .scan_tab import build_scan_tab
280
+
281
+ scan_state = build_scan_tab(store)
282
+ state["scan_state"] = scan_state
283
+ elif page_id == "graphql":
284
+ from .graphql_tab import build_graphql_tab
285
+
286
+ gql_state = build_graphql_tab(store)
287
+ state["gql_state"] = gql_state
288
+ elif page_id == "analytics":
289
+ from .analytics_tab import build_analytics_tab
290
+
291
+ build_analytics_tab(store)
292
+ elif page_id == "openapi":
293
+ from .openapi_tab import build_openapi_tab
294
+
295
+ build_openapi_tab(store)
296
+ elif page_id == "codegen":
297
+ from .codegen_tab import build_codegen_tab
298
+
299
+ codegen_state = build_codegen_tab(store)
300
+ state["codegen_state"] = codegen_state
301
+ elif page_id == "frida":
302
+ from .frida_tab import build_frida_tab
303
+
304
+ frida_state = build_frida_tab(store)
305
+ state["frida_state"] = frida_state
306
+ elif page_id == "sessions":
307
+ from .session_tab import build_session_tab
308
+
309
+ session_state = build_session_tab(store)
310
+ state["session_state"] = session_state
311
+ elif page_id == "report":
312
+ from .report_tab import build_report_tab
313
+
314
+ build_report_tab(store)
315
+ elif page_id == "import":
316
+ from .import_tab import build_import_tab
317
+
318
+ build_import_tab(store)
319
+
320
+
321
+ def _build_traffic_page(store: Store, state: dict, intercept_mgr) -> None:
322
+ with ui.splitter(value=58).classes("w-full").style("height:calc(100vh - 48px)") as sp:
323
+ with sp.before:
324
+ table = _build_traffic_table(store, state)
325
+ state["traffic_table"] = table
326
+
327
+ with sp.after, ui.element("div").classes("pp-detail"):
328
+ detail_col = ui.column().classes("w-full h-full")
329
+ state["detail_col"] = detail_col
330
+ _render_empty_detail(detail_col)
331
+
332
+
333
+ def _build_traffic_table(store: Store, state: dict) -> ui.table:
334
+ columns = [
335
+ {
336
+ "name": "id",
337
+ "label": "#",
338
+ "field": "id",
339
+ "align": "right",
340
+ "style": "width:42px; color:var(--pp-muted)",
341
+ },
342
+ {
343
+ "name": "method",
344
+ "label": "Method",
345
+ "field": "method",
346
+ "align": "center",
347
+ "style": "width:72px",
348
+ },
349
+ {"name": "host", "label": "Host", "field": "host", "align": "left"},
350
+ {"name": "path", "label": "Path", "field": "path", "align": "left"},
351
+ {
352
+ "name": "status",
353
+ "label": "Status",
354
+ "field": "status_code",
355
+ "align": "center",
356
+ "style": "width:64px",
357
+ },
358
+ {
359
+ "name": "size",
360
+ "label": "Size",
361
+ "field": "size",
362
+ "align": "right",
363
+ "style": "width:64px; color:var(--pp-muted)",
364
+ },
365
+ {
366
+ "name": "ms",
367
+ "label": "ms",
368
+ "field": "duration_ms",
369
+ "align": "right",
370
+ "style": "width:52px; color:var(--pp-muted)",
371
+ },
372
+ ]
373
+
374
+ table = (
375
+ ui.table(columns=columns, rows=[], row_key="id")
376
+ .classes("w-full pp-traffic")
377
+ .props("dense flat dark virtual-scroll")
378
+ .style("height:calc(100vh - 48px); overflow-y:auto")
379
+ )
380
+ table.add_slot(
381
+ "body",
382
+ r"""
383
+ <q-tr :props="props"
384
+ :class="{'pp-row-selected': props.row._selected}"
385
+ :style="props.row.color ? 'border-left:2px solid ' + props.row.color : ''"
386
+ @click="$emit('row-click', $event, props.row)"
387
+ @contextmenu.prevent="$emit('row-contextmenu', $event, props.row)"
388
+ style="cursor:pointer">
389
+ <q-td key="id" :props="props" style="color:var(--pp-muted); font-size:11px; font-family:monospace">{{ props.row.id }}</q-td>
390
+ <q-td key="method" :props="props">
391
+ <span :class="'m-pill m-' + props.row.method.toLowerCase()">{{ props.row.method }}</span>
392
+ </q-td>
393
+ <q-td key="host" :props="props" style="font-size:12px; max-width:200px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap">{{ props.row.host }}</q-td>
394
+ <q-td key="path" :props="props" style="font-size:12px; font-family:monospace; max-width:300px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap">{{ props.row.path }}</q-td>
395
+ <q-td key="status" :props="props">
396
+ <span v-if="props.row.status_code" :class="'s-pill s-' + Math.floor(props.row.status_code/100)">{{ props.row.status_code }}</span>
397
+ </q-td>
398
+ <q-td key="size" :props="props" style="font-size:11px; font-family:monospace">{{ props.row.size }}</q-td>
399
+ <q-td key="ms" :props="props" style="font-size:11px; font-family:monospace">{{ props.row.duration_ms }}</q-td>
400
+ </q-tr>
401
+ """,
402
+ )
403
+
404
+ # Load entries
405
+ entries, _ = store.list(state["filter"], 0, 500)
406
+ state["entries"] = list(reversed(entries))
407
+ _update_table(state["entries"], table)
408
+
409
+ # Row click → detail
410
+ async def on_row_click(e) -> None: # noqa: ANN001
411
+ try:
412
+ row = e.args[1] if isinstance(e.args, list) else e.args
413
+ entry_id = int(row["id"])
414
+ except (IndexError, KeyError, TypeError, ValueError):
415
+ return
416
+ entry = store.get(entry_id)
417
+ if entry:
418
+ state["selected"] = entry
419
+ # Mark selected
420
+ for r in table.rows:
421
+ r["_selected"] = r["id"] == entry_id
422
+ table.update()
423
+ if "detail_col" in state:
424
+ from .detail import render_detail
425
+
426
+ render_detail(entry, state["detail_col"])
427
+
428
+ table.on("row-click", on_row_click)
429
+
430
+ # Row right-click → context menu
431
+ async def on_row_contextmenu(e) -> None: # noqa: ANN001
432
+ try:
433
+ row = e.args[1] if isinstance(e.args, list) else e.args
434
+ entry_id = int(row["id"])
435
+ except (IndexError, KeyError, TypeError, ValueError):
436
+ return
437
+ entry = store.get(entry_id)
438
+ if not entry:
439
+ return
440
+ _show_context_menu(entry, state, store)
441
+
442
+ table.on("row-contextmenu", on_row_contextmenu)
443
+ return table
444
+
445
+
446
+ def _show_context_menu(entry: Entry, state: dict, store: Store) -> None:
447
+ with ui.menu() as menu:
448
+ # Tool shortcuts
449
+ _menu_item(menu, "send", "Resender", lambda: _send_to("resender", entry, state))
450
+ _menu_item(menu, "dynamic_feed", "Bulk Sender", lambda: _send_to("bulk", entry, state))
451
+ _menu_item(menu, "playlist_play", "Macro", lambda: _send_to("macro", entry, state))
452
+ _menu_item(menu, "compare", "A/B Test", lambda: _send_to("ab", entry, state))
453
+ _menu_item(menu, "code", "Generate Code", lambda: _send_to("codegen", entry, state))
454
+ _menu_item(menu, "security", "Security Check", lambda: _send_to("security", entry, state))
455
+ _menu_item(menu, "shield", "Adv Security", lambda: _send_to("advsec", entry, state))
456
+ _menu_item(menu, "search", "Active Scan", lambda: _send_to("scan", entry, state))
457
+ _menu_item(menu, "adb", "Frida Hook", lambda: _send_to("frida", entry, state))
458
+ if "graphql" in (entry.tags or []):
459
+ _menu_item(menu, "account_tree", "GraphQL", lambda: _send_to("graphql", entry, state))
460
+ _menu_item(menu, "folder", "Add to Session", lambda: _add_to_session(entry, state))
461
+
462
+ ui.separator()
463
+ _menu_item(menu, "difference", "Set Diff left", lambda: _set_diff_left(entry, state))
464
+ if state.get("compare_left"):
465
+ _menu_item(
466
+ menu, "difference", "Diff with left", lambda: _compare_diff(entry, state, store)
467
+ )
468
+
469
+ ui.separator()
470
+ ui.element("div").style(
471
+ "font-size:10px; color:var(--pp-muted); padding:4px 12px 2px; font-weight:700; text-transform:uppercase; letter-spacing:0.07em"
472
+ ).text = "Color"
473
+ with ui.row().classes("q-px-sm q-pb-xs gap-1"):
474
+ for color, label in zip(_ROW_COLORS, _COLOR_LABELS, strict=False):
475
+
476
+ def _set(c=color, eid=entry.id) -> None:
477
+ store.set_color(eid, c)
478
+ _refresh_table_rows(store, state)
479
+ menu.close()
480
+
481
+ dot = (
482
+ ui.element("div")
483
+ .style(
484
+ f"width:16px; height:16px; border-radius:3px; cursor:pointer; "
485
+ f"background:{color if color else 'var(--pp-surface2)'}; "
486
+ f"border:1px solid var(--pp-border)"
487
+ )
488
+ .tooltip(label)
489
+ )
490
+ dot.on("click", _set)
491
+ menu.open()
492
+
493
+
494
+ def _menu_item(menu: ui.menu, icon: str, label: str, handler) -> None:
495
+ with ui.menu_item(on_click=handler), ui.row().classes("items-center gap-2"):
496
+ ui.html(
497
+ f'<span class="material-icons" style="font-size:14px; color:var(--pp-muted)">{icon}</span>'
498
+ )
499
+ ui.element("span").style("font-size:13px").text = label
500
+
501
+
502
+ def _send_to(page_id: str, entry: Entry, state: dict) -> None:
503
+ state["pending_entry"] = entry
504
+ ui.notify(f"Opening {page_id}…", type="info", timeout=1000)
505
+ # Navigation handled by sidebar click
506
+
507
+
508
+ def _add_to_session(entry: Entry, state: dict) -> None:
509
+ from .session_tab import get_session_manager
510
+
511
+ mgr = get_session_manager()
512
+ active = mgr.get_active()
513
+ if active:
514
+ mgr.add_entry(active.id, entry.id)
515
+ ui.notify(f"Added to '{active.name}'", type="positive")
516
+ else:
517
+ ui.notify("No active session", type="warning")
518
+
519
+
520
+ def _set_diff_left(entry: Entry, state: dict) -> None:
521
+ state["compare_left"] = entry
522
+ ui.notify(f"#{entry.id} set as diff left", type="info")
523
+
524
+
525
+ def _compare_diff(entry: Entry, state: dict, store: Store) -> None:
526
+ left = state.get("compare_left")
527
+ if left and "diff_state" in state:
528
+ state["diff_state"]["set_entries"](left, entry)
529
+
530
+
531
+ def _render_empty_detail(container: ui.element) -> None:
532
+ container.clear()
533
+ with (
534
+ container,
535
+ ui.element("div").classes("pp-empty").style("height:100%; justify-content:center"),
536
+ ):
537
+ ui.html(
538
+ '<span class="material-icons" style="font-size:40px; color:var(--pp-muted); opacity:0.3">preview</span>'
539
+ )
540
+ ui.element("div").style(
541
+ "font-size:13px; color:var(--pp-muted)"
542
+ ).text = "Click a request to inspect"
543
+
544
+
545
+ def _update_table(entries: list[Entry], table: ui.table) -> None:
546
+ table.rows = [_entry_to_row(e) for e in entries]
547
+ table.update()
548
+
549
+
550
+ def _refresh_table_rows(store: Store, state: dict) -> None:
551
+ entries, _ = store.list(state["filter"], 0, 500)
552
+ state["entries"] = list(reversed(entries))
553
+ if "traffic_table" in state:
554
+ _update_table(state["entries"], state["traffic_table"])
555
+
556
+
557
+ def _entry_to_row(e: Entry) -> dict:
558
+ size = len(e.resp_body) if e.resp_body else 0
559
+ return {
560
+ "id": e.id,
561
+ "method": e.method,
562
+ "host": e.host,
563
+ "path": e.path + (f"?{e.query}" if e.query else ""),
564
+ "status_code": e.status_code,
565
+ "size": f"{size:,}" if size else "",
566
+ "duration_ms": e.duration_ms or "",
567
+ "protocol": e.protocol,
568
+ "color": getattr(e, "color", ""),
569
+ "_selected": False,
570
+ }
571
+
572
+
573
+ async def _clear_traffic(store: Store, state: dict) -> None:
574
+ store.clear()
575
+ state["entries"] = []
576
+ state["selected"] = None
577
+ if "traffic_table" in state:
578
+ state["traffic_table"].rows = []
579
+ state["traffic_table"].update()
580
+ if "detail_col" in state:
581
+ _render_empty_detail(state["detail_col"])
582
+ ui.notify("Cleared", type="info")