pypproxy 0.1.0__py3-none-any.whl
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.
- pypproxy/__init__.py +0 -0
- pypproxy/api/__init__.py +0 -0
- pypproxy/api/server.py +427 -0
- pypproxy/bulk/__init__.py +0 -0
- pypproxy/bulk/sender.py +97 -0
- pypproxy/cert/__init__.py +0 -0
- pypproxy/cert/ca.py +144 -0
- pypproxy/cert/client_cert.py +65 -0
- pypproxy/codec.py +176 -0
- pypproxy/config/__init__.py +0 -0
- pypproxy/config/config.py +106 -0
- pypproxy/dns/__init__.py +0 -0
- pypproxy/dns/server.py +149 -0
- pypproxy/exporter/__init__.py +0 -0
- pypproxy/exporter/exporter.py +122 -0
- pypproxy/exporter/importer.py +169 -0
- pypproxy/graphql/__init__.py +0 -0
- pypproxy/graphql/detector.py +76 -0
- pypproxy/graphql/introspection.py +217 -0
- pypproxy/graphql/modifier.py +98 -0
- pypproxy/graphql/schema_store.py +33 -0
- pypproxy/intercept/__init__.py +0 -0
- pypproxy/intercept/manager.py +142 -0
- pypproxy/interceptor/__init__.py +0 -0
- pypproxy/interceptor/interceptor.py +172 -0
- pypproxy/proto/__init__.py +0 -0
- pypproxy/proto/grpc.py +48 -0
- pypproxy/proto/mqtt.py +119 -0
- pypproxy/proto/ws.py +120 -0
- pypproxy/proto/ws_intercept.py +117 -0
- pypproxy/proxy/__init__.py +0 -0
- pypproxy/proxy/proxy.py +407 -0
- pypproxy/replay/__init__.py +0 -0
- pypproxy/replay/replay.py +77 -0
- pypproxy/rule/__init__.py +0 -0
- pypproxy/rule/rule.py +198 -0
- pypproxy/scan/__init__.py +0 -0
- pypproxy/scan/scanner.py +296 -0
- pypproxy/script/__init__.py +0 -0
- pypproxy/script/engine.py +49 -0
- pypproxy/security/__init__.py +0 -0
- pypproxy/security/header_checker.py +308 -0
- pypproxy/security/int_overflow.py +193 -0
- pypproxy/security/jwt_checker.py +273 -0
- pypproxy/security/plugin.py +152 -0
- pypproxy/security/randomness.py +165 -0
- pypproxy/store/__init__.py +0 -0
- pypproxy/store/db.py +189 -0
- pypproxy/store/filter_parser.py +181 -0
- pypproxy/store/fts.py +105 -0
- pypproxy/store/models.py +81 -0
- pypproxy/store/scope.py +63 -0
- pypproxy/store/store.py +120 -0
- pypproxy/ui/__init__.py +0 -0
- pypproxy/ui/app.py +386 -0
- pypproxy/ui/bulk_sender_ui.py +125 -0
- pypproxy/ui/cui.py +162 -0
- pypproxy/ui/detail.py +179 -0
- pypproxy/ui/diff_view.py +118 -0
- pypproxy/ui/graphql_tab.py +265 -0
- pypproxy/ui/import_tab.py +136 -0
- pypproxy/ui/intercept_dialog.py +74 -0
- pypproxy/ui/resender.py +140 -0
- pypproxy/ui/scan_tab.py +98 -0
- pypproxy/ui/security_tab.py +356 -0
- pypproxy/ui/settings.py +413 -0
- pypproxy/ui/theme.py +59 -0
- pypproxy-0.1.0.dist-info/METADATA +19 -0
- pypproxy-0.1.0.dist-info/RECORD +72 -0
- pypproxy-0.1.0.dist-info/WHEEL +4 -0
- pypproxy-0.1.0.dist-info/entry_points.txt +2 -0
- pypproxy-0.1.0.dist-info/licenses/LICENSE +21 -0
pypproxy/ui/settings.py
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from nicegui import ui
|
|
4
|
+
|
|
5
|
+
from pypproxy.cert.client_cert import ClientCert, ClientCertManager
|
|
6
|
+
from pypproxy.config.config import Config
|
|
7
|
+
from pypproxy.dns.server import DNSServer
|
|
8
|
+
from pypproxy.rule.rule import RuleManager
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def build_settings_page(
|
|
12
|
+
cfg: Config,
|
|
13
|
+
rules: RuleManager,
|
|
14
|
+
cert_mgr: ClientCertManager,
|
|
15
|
+
dns_server: DNSServer | None = None,
|
|
16
|
+
scope_mgr=None,
|
|
17
|
+
) -> None:
|
|
18
|
+
@ui.page("/settings")
|
|
19
|
+
async def settings() -> None:
|
|
20
|
+
ui.dark_mode().enable()
|
|
21
|
+
|
|
22
|
+
with ui.header().classes("items-center q-px-md gap-4").style("background:#1a1a2e"):
|
|
23
|
+
ui.button(icon="arrow_back", on_click=lambda: ui.navigate.to("/")).props("flat dark")
|
|
24
|
+
ui.label("Settings").classes("text-h6 text-weight-bold")
|
|
25
|
+
|
|
26
|
+
with ui.tabs().props("dense dark").classes("bg-dark") as tabs:
|
|
27
|
+
rules_tab = ui.tab("Rules", icon="rule")
|
|
28
|
+
scope_tab = ui.tab("Scope", icon="target")
|
|
29
|
+
passthrough_tab = ui.tab("SSL Passthrough", icon="lock_open")
|
|
30
|
+
dns_tab = ui.tab("DNS Overwrite", icon="dns")
|
|
31
|
+
ports_tab = ui.tab("Listen Ports", icon="router")
|
|
32
|
+
certs_tab = ui.tab("Client Certs", icon="badge")
|
|
33
|
+
|
|
34
|
+
with (
|
|
35
|
+
ui.tab_panels(tabs, value=rules_tab)
|
|
36
|
+
.classes("w-full")
|
|
37
|
+
.style("height:calc(100vh - 96px)")
|
|
38
|
+
):
|
|
39
|
+
# --- Rules tab ---
|
|
40
|
+
with ui.tab_panel(rules_tab).classes("q-pa-md"):
|
|
41
|
+
_build_rules_panel(rules)
|
|
42
|
+
|
|
43
|
+
# --- Scope tab ---
|
|
44
|
+
with ui.tab_panel(scope_tab).classes("q-pa-md"):
|
|
45
|
+
_build_scope_panel(scope_mgr)
|
|
46
|
+
|
|
47
|
+
# --- SSL Passthrough tab ---
|
|
48
|
+
with ui.tab_panel(passthrough_tab).classes("q-pa-md"):
|
|
49
|
+
_build_passthrough_panel(cfg)
|
|
50
|
+
|
|
51
|
+
# --- DNS Overwrite tab ---
|
|
52
|
+
with ui.tab_panel(dns_tab).classes("q-pa-md"):
|
|
53
|
+
_build_dns_panel(cfg, dns_server)
|
|
54
|
+
|
|
55
|
+
# --- Listen Ports tab ---
|
|
56
|
+
with ui.tab_panel(ports_tab).classes("q-pa-md"):
|
|
57
|
+
_build_ports_panel(cfg)
|
|
58
|
+
|
|
59
|
+
# --- Client Certs tab ---
|
|
60
|
+
with ui.tab_panel(certs_tab).classes("q-pa-md"):
|
|
61
|
+
_build_certs_panel(cert_mgr)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _build_scope_panel(scope_mgr) -> None: # noqa: ANN001
|
|
65
|
+
from pypproxy.store.scope import ScopeRule
|
|
66
|
+
|
|
67
|
+
ui.label("Scope").classes("text-subtitle1 q-mb-sm")
|
|
68
|
+
ui.label(
|
|
69
|
+
"When enabled, only in-scope hosts are captured. All others are proxied without recording."
|
|
70
|
+
).classes("text-caption text-grey q-mb-md")
|
|
71
|
+
|
|
72
|
+
if scope_mgr is None:
|
|
73
|
+
ui.label("Scope manager not initialized.").classes("text-grey")
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
enabled_toggle = ui.switch("Enable scope filtering", value=scope_mgr.enabled).props(
|
|
77
|
+
"dense dark color=primary"
|
|
78
|
+
)
|
|
79
|
+
enabled_toggle.on("update:model-value", lambda e: scope_mgr.set_enabled(e.args))
|
|
80
|
+
|
|
81
|
+
ui.separator().classes("q-my-md")
|
|
82
|
+
container = ui.column().classes("w-full")
|
|
83
|
+
|
|
84
|
+
def _refresh() -> None:
|
|
85
|
+
container.clear()
|
|
86
|
+
with container:
|
|
87
|
+
for rule in scope_mgr.list():
|
|
88
|
+
with ui.row().classes("items-center gap-2"):
|
|
89
|
+
ui.badge(rule.mode, color="grey-7")
|
|
90
|
+
ui.label(rule.pattern).classes("flex-1 font-mono")
|
|
91
|
+
ui.button(
|
|
92
|
+
icon="remove",
|
|
93
|
+
on_click=lambda p=rule.pattern: (scope_mgr.remove(p), _refresh()),
|
|
94
|
+
).props("flat dense color=negative size=sm")
|
|
95
|
+
|
|
96
|
+
_refresh()
|
|
97
|
+
|
|
98
|
+
with ui.row().classes("gap-2 items-center q-mt-sm"):
|
|
99
|
+
pattern_input = (
|
|
100
|
+
ui.input(label="Pattern (e.g. *.example.com)")
|
|
101
|
+
.props("dense outlined dark")
|
|
102
|
+
.classes("w-64")
|
|
103
|
+
)
|
|
104
|
+
mode_select = (
|
|
105
|
+
ui.select(["glob", "regex"], value="glob", label="Mode")
|
|
106
|
+
.props("dense outlined dark")
|
|
107
|
+
.classes("w-24")
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def _add() -> None:
|
|
111
|
+
p = pattern_input.value.strip()
|
|
112
|
+
if p:
|
|
113
|
+
scope_mgr.add(ScopeRule(pattern=p, mode=mode_select.value))
|
|
114
|
+
pattern_input.value = ""
|
|
115
|
+
_refresh()
|
|
116
|
+
|
|
117
|
+
ui.button("Add", icon="add", on_click=_add).props("color=primary size=sm")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _build_rules_panel(rules: RuleManager) -> None:
|
|
121
|
+
ui.label("Intercept Rules").classes("text-subtitle1 q-mb-sm")
|
|
122
|
+
ui.label("Rules are evaluated in priority order. Higher number = higher priority.").classes(
|
|
123
|
+
"text-caption text-grey q-mb-md"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
rules_container = ui.column().classes("w-full")
|
|
127
|
+
|
|
128
|
+
def _refresh() -> None:
|
|
129
|
+
rules_container.clear()
|
|
130
|
+
with rules_container:
|
|
131
|
+
for rule in rules.list():
|
|
132
|
+
with (
|
|
133
|
+
ui.card().classes("w-full q-mb-xs"),
|
|
134
|
+
ui.row().classes("items-center gap-2 w-full"),
|
|
135
|
+
):
|
|
136
|
+
ui.switch(value=rule.enabled).on(
|
|
137
|
+
"update:model-value",
|
|
138
|
+
lambda v, r=rule: (setattr(r, "enabled", v.args), rules.update(r)),
|
|
139
|
+
)
|
|
140
|
+
ui.label(rule.name).classes("text-weight-medium flex-1")
|
|
141
|
+
ui.badge(
|
|
142
|
+
rule.action.value,
|
|
143
|
+
color={
|
|
144
|
+
"block": "negative",
|
|
145
|
+
"modify": "warning",
|
|
146
|
+
"redirect": "info",
|
|
147
|
+
"passthrough": "positive",
|
|
148
|
+
}.get(rule.action.value, "grey"),
|
|
149
|
+
)
|
|
150
|
+
ui.label(f"p={rule.priority}").classes("text-caption text-grey")
|
|
151
|
+
ui.button(
|
|
152
|
+
icon="delete",
|
|
153
|
+
on_click=lambda r=rule: (rules.delete(r.id), _refresh()),
|
|
154
|
+
).props("flat dense color=negative size=sm")
|
|
155
|
+
|
|
156
|
+
_refresh()
|
|
157
|
+
|
|
158
|
+
ui.separator().classes("q-my-md")
|
|
159
|
+
ui.label("Add Rule").classes("text-subtitle2")
|
|
160
|
+
with ui.row().classes("gap-2 items-end q-mt-sm"):
|
|
161
|
+
name_input = ui.input(label="Name").props("dense outlined dark").classes("w-48")
|
|
162
|
+
action_select = (
|
|
163
|
+
ui.select(
|
|
164
|
+
["block", "passthrough", "modify", "redirect"],
|
|
165
|
+
value="block",
|
|
166
|
+
label="Action",
|
|
167
|
+
)
|
|
168
|
+
.props("dense outlined dark")
|
|
169
|
+
.classes("w-32")
|
|
170
|
+
)
|
|
171
|
+
priority_input = (
|
|
172
|
+
ui.number(label="Priority", value=0).props("dense outlined dark").classes("w-24")
|
|
173
|
+
)
|
|
174
|
+
field_select = (
|
|
175
|
+
ui.select(
|
|
176
|
+
["host", "path", "method", "header", "body"],
|
|
177
|
+
value="host",
|
|
178
|
+
label="Field",
|
|
179
|
+
)
|
|
180
|
+
.props("dense outlined dark")
|
|
181
|
+
.classes("w-24")
|
|
182
|
+
)
|
|
183
|
+
op_select = (
|
|
184
|
+
ui.select(
|
|
185
|
+
["contains", "equals", "prefix", "regex"],
|
|
186
|
+
value="contains",
|
|
187
|
+
label="Op",
|
|
188
|
+
)
|
|
189
|
+
.props("dense outlined dark")
|
|
190
|
+
.classes("w-28")
|
|
191
|
+
)
|
|
192
|
+
value_input = ui.input(label="Value").props("dense outlined dark").classes("w-48")
|
|
193
|
+
|
|
194
|
+
def _add() -> None:
|
|
195
|
+
from pypproxy.rule.rule import Condition, MatchField, Rule
|
|
196
|
+
|
|
197
|
+
if not name_input.value or not value_input.value:
|
|
198
|
+
ui.notify("Name and Value are required", type="warning")
|
|
199
|
+
return
|
|
200
|
+
rule = Rule(
|
|
201
|
+
name=name_input.value,
|
|
202
|
+
enabled=True,
|
|
203
|
+
priority=int(priority_input.value or 0),
|
|
204
|
+
action=action_select.value,
|
|
205
|
+
conditions=[
|
|
206
|
+
Condition(
|
|
207
|
+
field=MatchField(field_select.value),
|
|
208
|
+
op=op_select.value,
|
|
209
|
+
value=value_input.value,
|
|
210
|
+
)
|
|
211
|
+
],
|
|
212
|
+
)
|
|
213
|
+
rules.add(rule)
|
|
214
|
+
name_input.value = ""
|
|
215
|
+
value_input.value = ""
|
|
216
|
+
_refresh()
|
|
217
|
+
ui.notify(f"Rule '{rule.name}' added", type="positive")
|
|
218
|
+
|
|
219
|
+
ui.button("Add", icon="add", on_click=_add).props("color=primary size=sm")
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _build_passthrough_panel(cfg: Config) -> None:
|
|
223
|
+
ui.label("SSL Passthrough").classes("text-subtitle1 q-mb-sm")
|
|
224
|
+
ui.label(
|
|
225
|
+
"Hosts listed here will be tunneled without TLS interception (for certificate pinning, etc.)."
|
|
226
|
+
).classes("text-caption text-grey q-mb-md")
|
|
227
|
+
|
|
228
|
+
container = ui.column().classes("w-full")
|
|
229
|
+
|
|
230
|
+
def _refresh() -> None:
|
|
231
|
+
container.clear()
|
|
232
|
+
with container:
|
|
233
|
+
for host in cfg.proxy.ignore:
|
|
234
|
+
with ui.row().classes("items-center gap-2"):
|
|
235
|
+
ui.label(host).classes("flex-1 font-mono")
|
|
236
|
+
ui.button(
|
|
237
|
+
icon="remove",
|
|
238
|
+
on_click=lambda h=host: (cfg.proxy.ignore.remove(h), _refresh()),
|
|
239
|
+
).props("flat dense color=negative size=sm")
|
|
240
|
+
|
|
241
|
+
_refresh()
|
|
242
|
+
|
|
243
|
+
with ui.row().classes("gap-2 items-center q-mt-sm"):
|
|
244
|
+
host_input = ui.input(label="Host").props("dense outlined dark").classes("w-64")
|
|
245
|
+
|
|
246
|
+
def _add() -> None:
|
|
247
|
+
h = host_input.value.strip()
|
|
248
|
+
if h and h not in cfg.proxy.ignore:
|
|
249
|
+
cfg.proxy.ignore.append(h)
|
|
250
|
+
host_input.value = ""
|
|
251
|
+
_refresh()
|
|
252
|
+
|
|
253
|
+
ui.button("Add", icon="add", on_click=_add).props("color=primary size=sm")
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _build_dns_panel(cfg: Config, dns_server: DNSServer | None) -> None:
|
|
257
|
+
ui.label("DNS Overwrite").classes("text-subtitle1 q-mb-sm")
|
|
258
|
+
ui.label(
|
|
259
|
+
"Redirect specific domains to a target IP. Start the DNS server and point devices to this machine."
|
|
260
|
+
).classes("text-caption text-grey q-mb-md")
|
|
261
|
+
|
|
262
|
+
overrides: dict[str, str] = {}
|
|
263
|
+
container = ui.column().classes("w-full")
|
|
264
|
+
|
|
265
|
+
with ui.row().classes("items-center gap-2 q-mb-md"):
|
|
266
|
+
dns_status = ui.badge("Stopped", color="grey")
|
|
267
|
+
if dns_server:
|
|
268
|
+
ui.button(
|
|
269
|
+
"Start DNS",
|
|
270
|
+
icon="play_arrow",
|
|
271
|
+
on_click=lambda: _start_dns(dns_server, dns_status, overrides),
|
|
272
|
+
).props("size=sm color=positive")
|
|
273
|
+
ui.label("Port 53153 (use iptables/pfctl to redirect port 53)").classes(
|
|
274
|
+
"text-caption text-grey"
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
def _refresh() -> None:
|
|
278
|
+
container.clear()
|
|
279
|
+
with container:
|
|
280
|
+
for domain, ip in overrides.items():
|
|
281
|
+
with ui.row().classes("items-center gap-2"):
|
|
282
|
+
ui.label(domain).classes("flex-1 font-mono")
|
|
283
|
+
ui.label("→").classes("text-grey")
|
|
284
|
+
ui.label(ip).classes("font-mono text-positive")
|
|
285
|
+
ui.button(
|
|
286
|
+
icon="remove",
|
|
287
|
+
on_click=lambda d=domain: (
|
|
288
|
+
overrides.pop(d, None),
|
|
289
|
+
_refresh_dns(dns_server, overrides),
|
|
290
|
+
_refresh(),
|
|
291
|
+
),
|
|
292
|
+
).props("flat dense color=negative size=sm")
|
|
293
|
+
|
|
294
|
+
_refresh()
|
|
295
|
+
|
|
296
|
+
with ui.row().classes("gap-2 items-center q-mt-sm"):
|
|
297
|
+
domain_input = ui.input(label="Domain").props("dense outlined dark").classes("w-48")
|
|
298
|
+
ip_input = ui.input(label="Target IP").props("dense outlined dark").classes("w-36")
|
|
299
|
+
|
|
300
|
+
def _add() -> None:
|
|
301
|
+
d, ip = domain_input.value.strip(), ip_input.value.strip()
|
|
302
|
+
if d and ip:
|
|
303
|
+
overrides[d] = ip
|
|
304
|
+
domain_input.value = ""
|
|
305
|
+
ip_input.value = ""
|
|
306
|
+
_refresh_dns(dns_server, overrides)
|
|
307
|
+
_refresh()
|
|
308
|
+
|
|
309
|
+
ui.button("Add", icon="add", on_click=_add).props("color=primary size=sm")
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
async def _start_dns(server: DNSServer, badge: ui.badge, overrides: dict) -> None:
|
|
313
|
+
try:
|
|
314
|
+
server.set_overrides(overrides)
|
|
315
|
+
await server.start()
|
|
316
|
+
badge.text = "Running"
|
|
317
|
+
badge.props("color=positive")
|
|
318
|
+
ui.notify("DNS server started on :53153", type="positive")
|
|
319
|
+
except Exception as e:
|
|
320
|
+
ui.notify(f"Failed to start DNS: {e}", type="negative")
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _refresh_dns(server: DNSServer | None, overrides: dict) -> None:
|
|
324
|
+
if server:
|
|
325
|
+
server.set_overrides(overrides)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _build_ports_panel(cfg: Config) -> None:
|
|
329
|
+
ui.label("Listen Ports").classes("text-subtitle1 q-mb-sm")
|
|
330
|
+
ui.label("Configure proxy listen address and port.").classes("text-caption text-grey q-mb-md")
|
|
331
|
+
|
|
332
|
+
with ui.row().classes("gap-4 items-end"):
|
|
333
|
+
addr_input = (
|
|
334
|
+
ui.input(label="Proxy Address", value=cfg.proxy.addr)
|
|
335
|
+
.props("dense outlined dark")
|
|
336
|
+
.classes("w-48")
|
|
337
|
+
)
|
|
338
|
+
port_input = (
|
|
339
|
+
ui.number(label="Proxy Port", value=cfg.proxy.port)
|
|
340
|
+
.props("dense outlined dark")
|
|
341
|
+
.classes("w-28")
|
|
342
|
+
)
|
|
343
|
+
ui_port_input = (
|
|
344
|
+
ui.number(label="UI Port", value=cfg.ui.port)
|
|
345
|
+
.props("dense outlined dark")
|
|
346
|
+
.classes("w-28")
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
def _save() -> None:
|
|
350
|
+
cfg.proxy.addr = addr_input.value
|
|
351
|
+
cfg.proxy.port = int(port_input.value or 8080)
|
|
352
|
+
cfg.ui.port = int(ui_port_input.value or 8081)
|
|
353
|
+
ui.notify("Port settings saved (takes effect on restart)", type="info")
|
|
354
|
+
|
|
355
|
+
ui.button("Save", icon="save", on_click=_save).props("color=primary size=sm")
|
|
356
|
+
|
|
357
|
+
ui.label("Note: Port changes take effect after restarting paxy.").classes(
|
|
358
|
+
"text-caption text-grey q-mt-sm"
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _build_certs_panel(cert_mgr: ClientCertManager) -> None:
|
|
363
|
+
ui.label("Client Certificates").classes("text-subtitle1 q-mb-sm")
|
|
364
|
+
ui.label("Client certificates are sent to matching hosts during TLS handshake.").classes(
|
|
365
|
+
"text-caption text-grey q-mb-md"
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
container = ui.column().classes("w-full")
|
|
369
|
+
|
|
370
|
+
def _refresh() -> None:
|
|
371
|
+
container.clear()
|
|
372
|
+
with container:
|
|
373
|
+
for cert in cert_mgr.list():
|
|
374
|
+
with ui.card().classes("w-full q-mb-xs"), ui.row().classes("items-center gap-2"):
|
|
375
|
+
ui.icon("badge").classes("text-primary")
|
|
376
|
+
with ui.column().classes("flex-1"):
|
|
377
|
+
ui.label(cert.name).classes("text-weight-medium")
|
|
378
|
+
ui.label(f"{cert.host_pattern} • {cert.cert_path}").classes(
|
|
379
|
+
"text-caption text-grey"
|
|
380
|
+
)
|
|
381
|
+
ui.button(
|
|
382
|
+
icon="delete",
|
|
383
|
+
on_click=lambda n=cert.name: (cert_mgr.remove(n), _refresh()),
|
|
384
|
+
).props("flat dense color=negative size=sm")
|
|
385
|
+
|
|
386
|
+
_refresh()
|
|
387
|
+
|
|
388
|
+
ui.separator().classes("q-my-md")
|
|
389
|
+
ui.label("Add Client Certificate").classes("text-subtitle2")
|
|
390
|
+
with ui.grid(columns=2).classes("gap-2 q-mt-sm"):
|
|
391
|
+
name_input = ui.input(label="Name").props("dense outlined dark")
|
|
392
|
+
pattern_input = ui.input(label="Host Pattern", value="*").props("dense outlined dark")
|
|
393
|
+
cert_input = ui.input(label="Certificate Path (.pem)").props("dense outlined dark")
|
|
394
|
+
key_input = ui.input(label="Key Path (.pem)").props("dense outlined dark")
|
|
395
|
+
|
|
396
|
+
def _add() -> None:
|
|
397
|
+
if not name_input.value or not cert_input.value or not key_input.value:
|
|
398
|
+
ui.notify("All fields are required", type="warning")
|
|
399
|
+
return
|
|
400
|
+
cert_mgr.add(
|
|
401
|
+
ClientCert(
|
|
402
|
+
name=name_input.value,
|
|
403
|
+
cert_path=cert_input.value,
|
|
404
|
+
key_path=key_input.value,
|
|
405
|
+
host_pattern=pattern_input.value or "*",
|
|
406
|
+
)
|
|
407
|
+
)
|
|
408
|
+
for inp in (name_input, cert_input, key_input):
|
|
409
|
+
inp.value = ""
|
|
410
|
+
_refresh()
|
|
411
|
+
ui.notify("Certificate added", type="positive")
|
|
412
|
+
|
|
413
|
+
ui.button("Add", icon="add", on_click=_add).props("color=primary size=sm q-mt-sm")
|
pypproxy/ui/theme.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from nicegui import ui
|
|
2
|
+
|
|
3
|
+
METHOD_COLORS: dict[str, str] = {
|
|
4
|
+
"GET": "blue",
|
|
5
|
+
"POST": "green",
|
|
6
|
+
"PUT": "orange",
|
|
7
|
+
"PATCH": "purple",
|
|
8
|
+
"DELETE": "red",
|
|
9
|
+
"HEAD": "grey",
|
|
10
|
+
"OPTIONS": "grey",
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
STATUS_COLORS: dict[int, str] = {}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def status_color(code: int) -> str:
|
|
17
|
+
if 200 <= code < 300:
|
|
18
|
+
return "positive"
|
|
19
|
+
if 300 <= code < 400:
|
|
20
|
+
return "info"
|
|
21
|
+
if 400 <= code < 500:
|
|
22
|
+
return "warning"
|
|
23
|
+
if code >= 500:
|
|
24
|
+
return "negative"
|
|
25
|
+
return "grey"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def method_badge(method: str) -> None:
|
|
29
|
+
color = METHOD_COLORS.get(method.upper(), "grey")
|
|
30
|
+
ui.badge(method, color=color).props("rounded")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def status_badge(code: int) -> None:
|
|
34
|
+
if code == 0:
|
|
35
|
+
return
|
|
36
|
+
ui.badge(str(code), color=status_color(code)).props("rounded")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def apply_dark_theme() -> None:
|
|
40
|
+
ui.add_head_html("""
|
|
41
|
+
<style>
|
|
42
|
+
.paxy-row-modified { background: rgba(255, 200, 0, 0.08) !important; }
|
|
43
|
+
.paxy-row-blocked { background: rgba(255, 50, 50, 0.08) !important; }
|
|
44
|
+
.paxy-row-selected { background: rgba(100, 150, 255, 0.15) !important; }
|
|
45
|
+
.paxy-body-pre {
|
|
46
|
+
font-family: monospace;
|
|
47
|
+
font-size: 12px;
|
|
48
|
+
white-space: pre-wrap;
|
|
49
|
+
word-break: break-all;
|
|
50
|
+
max-height: 400px;
|
|
51
|
+
overflow-y: auto;
|
|
52
|
+
background: rgba(0,0,0,0.2);
|
|
53
|
+
padding: 8px;
|
|
54
|
+
border-radius: 4px;
|
|
55
|
+
}
|
|
56
|
+
.paxy-header-table td { padding: 2px 8px; font-size: 12px; }
|
|
57
|
+
.paxy-header-table td:first-child { color: #aaa; min-width: 160px; }
|
|
58
|
+
</style>
|
|
59
|
+
""")
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pypproxy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MITM HTTP/HTTPS proxy for inspecting and modifying traffic
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: aiosqlite>=0.20.0
|
|
8
|
+
Requires-Dist: brotli>=1.1.0
|
|
9
|
+
Requires-Dist: cbor2>=5.6.0
|
|
10
|
+
Requires-Dist: cryptography>=48.0.0
|
|
11
|
+
Requires-Dist: fastapi>=0.111.0
|
|
12
|
+
Requires-Dist: httpx>=0.27.0
|
|
13
|
+
Requires-Dist: httpx[http2]>=0.27.0
|
|
14
|
+
Requires-Dist: msgpack>=1.1.0
|
|
15
|
+
Requires-Dist: nicegui>=3.12.1
|
|
16
|
+
Requires-Dist: pyyaml>=6.0.3
|
|
17
|
+
Requires-Dist: rich>=15.0.0
|
|
18
|
+
Requires-Dist: uvicorn[standard]>=0.30.0
|
|
19
|
+
Requires-Dist: websockets>=16.0
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
pypproxy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
pypproxy/codec.py,sha256=VTwo3WutemcU3v5qjY_zCIS17P_vtqwXde3QcXfiKY0,5617
|
|
3
|
+
pypproxy/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
pypproxy/api/server.py,sha256=sRYu98BslZqFPLvw1fm6xW4GVMppivewFbS8_kWJxgA,12125
|
|
5
|
+
pypproxy/bulk/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
pypproxy/bulk/sender.py,sha256=t905m7vIliqKMckiKFLE4mC3JUxm7veQEcBJVTj4Fmg,2752
|
|
7
|
+
pypproxy/cert/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
pypproxy/cert/ca.py,sha256=O0C-Er_WCrQXemd-Km1xfPFcrlSsjRxydNgstFX-PfE,5314
|
|
9
|
+
pypproxy/cert/client_cert.py,sha256=zNMEXLL_OuoonFeG8MpJOAaoAfgMTrA3gVD6Jf7gjF4,1726
|
|
10
|
+
pypproxy/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
pypproxy/config/config.py,sha256=bfH7V-SZDtKzBtdzP3f7ST2LT6cq1Xv8p0fkvKvaqSc,2862
|
|
12
|
+
pypproxy/dns/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
pypproxy/dns/server.py,sha256=I1R084TjS-DuC01NIcPkXoEIkJglRKGivKeH3m5XeOk,4973
|
|
14
|
+
pypproxy/exporter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
pypproxy/exporter/exporter.py,sha256=96ZhTkk93HUxMFrwUfwrc8FyRVja2dNtRTDAJD2bZT4,3810
|
|
16
|
+
pypproxy/exporter/importer.py,sha256=oWFwRmA-4aUy4zeZbWanwbbNE1ICgxBmVHuu757N2ao,4991
|
|
17
|
+
pypproxy/graphql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
+
pypproxy/graphql/detector.py,sha256=jJgSLoHP1wOtRkVW4d0flsCsXZfp0Hv_tCjj7VZ49oc,2575
|
|
19
|
+
pypproxy/graphql/introspection.py,sha256=HpbEW4_a_n0FX-6EeDsiZfuJ6TXel22NjSn3TyQlAxc,5683
|
|
20
|
+
pypproxy/graphql/modifier.py,sha256=qzJ5fNavNKtI2102ZcLDpZQtT_WpcgROgIeh-cHwNiI,3093
|
|
21
|
+
pypproxy/graphql/schema_store.py,sha256=BQWf1sveEvUnk489yjFPt8LCL701W1CP-T2TdG68ER8,856
|
|
22
|
+
pypproxy/intercept/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
|
+
pypproxy/intercept/manager.py,sha256=_dBiqMotuvfN-7UMiP7vN2hCdteXQkcK9TJ9_VcH6As,4066
|
|
24
|
+
pypproxy/interceptor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
25
|
+
pypproxy/interceptor/interceptor.py,sha256=rSA_B2jKACPbAQCk_G7UUX1J8BcW-GR815rQHcv7cFQ,5108
|
|
26
|
+
pypproxy/proto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
|
+
pypproxy/proto/grpc.py,sha256=gZdkGA9LlnQMPs4lk-uzEouQgiCPFxfei3aMIYFRlKM,1278
|
|
28
|
+
pypproxy/proto/mqtt.py,sha256=7PUkcvEMiW56bwyOKCHNbnSNiLf_Ts6uq6ylWCk8kVo,2913
|
|
29
|
+
pypproxy/proto/ws.py,sha256=5SdhChNLC-KHUzJPXLem8vqm9DvDg2xRIden1N_AzEU,3230
|
|
30
|
+
pypproxy/proto/ws_intercept.py,sha256=2d_R_fYfAeaeP0JJq3YnkSKTH_zIWvJEN8UtogM-VPY,3388
|
|
31
|
+
pypproxy/proxy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
32
|
+
pypproxy/proxy/proxy.py,sha256=3oeEdJEipDTQ20nAu-Ka_RCCaZ3JRxjBOVnuZD38v1A,13118
|
|
33
|
+
pypproxy/replay/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
34
|
+
pypproxy/replay/replay.py,sha256=y3Yo9QVbufaT4VskKyxeWG59bZHwrg5RLa6jVOIRgK0,2100
|
|
35
|
+
pypproxy/rule/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
36
|
+
pypproxy/rule/rule.py,sha256=V7EJir77KhpSzmWViaReF6ttsxm6YiGtlBQzL2jx2TM,5763
|
|
37
|
+
pypproxy/scan/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
38
|
+
pypproxy/scan/scanner.py,sha256=OinfUDJQUfpf4IYm2VtScrtbbj60yoSAgnCLtUUeQ4E,8433
|
|
39
|
+
pypproxy/script/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
40
|
+
pypproxy/script/engine.py,sha256=YjN63iFqrguFuLlS7B1f0eUOcHfWS7bcCiGvGakBjqs,1607
|
|
41
|
+
pypproxy/security/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
42
|
+
pypproxy/security/header_checker.py,sha256=MUBH0J3R88fHr9JPcnpSLi6PdoBgySnjfzfjks7mCVg,8757
|
|
43
|
+
pypproxy/security/int_overflow.py,sha256=ZaaIqYwvSfrm02C6HweMLQ3s9n95Laeh0AieiWPCt0o,6321
|
|
44
|
+
pypproxy/security/jwt_checker.py,sha256=02Y3gLfBWfyVo8H-9vAbEoy07ZAu4JM9uJbdsOeJ1Qc,8606
|
|
45
|
+
pypproxy/security/plugin.py,sha256=65v6xLv9SBnuAapwrxDNH-V3DNH2KH9waqFEN4ZgMu4,4953
|
|
46
|
+
pypproxy/security/randomness.py,sha256=7T8X9JA3jZ58kMWcAW2oJV8_7d70IAQhGPzhpTwc9-c,4805
|
|
47
|
+
pypproxy/store/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
48
|
+
pypproxy/store/db.py,sha256=FhCZPH9g-G-3zSTaGoA60jYtp_ebAccnXejv3o3GPo0,6070
|
|
49
|
+
pypproxy/store/filter_parser.py,sha256=i0kHfAEUh4Ij0qiXHMYiguQavc80I6-I1oKq1Ylhoo0,5089
|
|
50
|
+
pypproxy/store/fts.py,sha256=98ZJgsro0WGDLtqdhx_HKWSv9TnUd9uZ2hp3RJeBONw,3142
|
|
51
|
+
pypproxy/store/models.py,sha256=HdeONRuosHt6YmzlGh6lLCYyvvC5jYPYdTF8jb8jNxQ,2633
|
|
52
|
+
pypproxy/store/scope.py,sha256=NOO1Nf4t8d3F8m6ZJLfhinlcvpHhUsh8hLS1-CBZEN0,1685
|
|
53
|
+
pypproxy/store/store.py,sha256=yk69W9ezw4JqK0s8MPHtjYAF9cRrf3Ypd41ruUtS9z0,3743
|
|
54
|
+
pypproxy/ui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
55
|
+
pypproxy/ui/app.py,sha256=khCpYrxk4-xuYIQj16XKcmD-aWlTGY2S4DqsSDIRkRc,14514
|
|
56
|
+
pypproxy/ui/bulk_sender_ui.py,sha256=LI9bwS_tTxMwUCfKpa0IgNypn7IWsGrphW4AmYLfBW4,4809
|
|
57
|
+
pypproxy/ui/cui.py,sha256=84oVg5s2ca1XEo_MyNmwU042wQpxLSr0Ra4AvGbzYwc,4674
|
|
58
|
+
pypproxy/ui/detail.py,sha256=B0pIf5Ep_cEtEdUdLf5t6m0S-hi51KvjhCno13foig0,6275
|
|
59
|
+
pypproxy/ui/diff_view.py,sha256=ewN9UmGGpxJf-8OtgFZ7YsRXWa__mEhI7pNWVUWgV4E,3823
|
|
60
|
+
pypproxy/ui/graphql_tab.py,sha256=Yw-0Bxzsrviu2PPZH00obbYKHv3ONrHvXj609gCKiIY,10658
|
|
61
|
+
pypproxy/ui/import_tab.py,sha256=RbzVEKyN2oG4hvcIqH63nBuurdLnF4T5L7Fsxa4jtlU,5226
|
|
62
|
+
pypproxy/ui/intercept_dialog.py,sha256=aSca1vX6R2DB6Krs7HnJ6hVQvic-cBJsoYitW1z1zDI,2666
|
|
63
|
+
pypproxy/ui/resender.py,sha256=obiFQ_dKZN6RFpNh82oLSna7YRVmS2xlFqBJP4R7Vyg,5182
|
|
64
|
+
pypproxy/ui/scan_tab.py,sha256=_OPjOLC6Gkz1oNFlqzcUdUjKsuLiBMjykzSeDozmF5E,3823
|
|
65
|
+
pypproxy/ui/security_tab.py,sha256=jl05lGgZtwv1sYuoRQMxlI64jB4PmYUwAE78tcJtL3Q,16292
|
|
66
|
+
pypproxy/ui/settings.py,sha256=W0Fl8xKdEi6ow5GRP9tYF7N5Nor-LMgRMfSTtDKAF_M,15744
|
|
67
|
+
pypproxy/ui/theme.py,sha256=Yda_pGxT-WgcGH1jWmW1fpm-NqjwHEBRBxOWVSflwLo,1541
|
|
68
|
+
pypproxy-0.1.0.dist-info/METADATA,sha256=OXYPv3FNFqLZ0nOg6F5-p4WpXMWoWhAicf56zV7XqWQ,580
|
|
69
|
+
pypproxy-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
70
|
+
pypproxy-0.1.0.dist-info/entry_points.txt,sha256=qGPuutdrIlY_LGUMNLmcBseWjgnGuNRX0P8oqnK1Es8,39
|
|
71
|
+
pypproxy-0.1.0.dist-info/licenses/LICENSE,sha256=hO5vafKkFOjcGnOjmv0SScAeG3c7jTDD7EFT_KOLiHA,1061
|
|
72
|
+
pypproxy-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kuma
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|