pypproxy 0.1.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 (141) hide show
  1. {pypproxy-0.1.0 → pypproxy-0.3.0}/PKG-INFO +3 -1
  2. {pypproxy-0.1.0 → pypproxy-0.3.0}/docs/api.md +1 -1
  3. {pypproxy-0.1.0 → pypproxy-0.3.0}/docs/architecture.md +40 -40
  4. {pypproxy-0.1.0 → pypproxy-0.3.0}/docs/configuration.md +1 -1
  5. {pypproxy-0.1.0 → pypproxy-0.3.0}/docs/getting-started.md +1 -1
  6. {pypproxy-0.1.0 → pypproxy-0.3.0}/docs/index.md +1 -1
  7. {pypproxy-0.1.0 → pypproxy-0.3.0}/docs/protocols.md +5 -5
  8. {pypproxy-0.1.0 → pypproxy-0.3.0}/docs/scripting.md +3 -3
  9. {pypproxy-0.1.0 → pypproxy-0.3.0}/docs/web-ui.md +2 -2
  10. pypproxy-0.3.0/pypproxy/ab_test/runner.py +136 -0
  11. pypproxy-0.3.0/pypproxy/analytics/stats.py +225 -0
  12. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/api/server.py +83 -0
  13. pypproxy-0.3.0/pypproxy/codec.py +427 -0
  14. pypproxy-0.3.0/pypproxy/codegen/generator.py +132 -0
  15. pypproxy-0.3.0/pypproxy/frida/device.py +203 -0
  16. pypproxy-0.3.0/pypproxy/frida/hook_generator.py +189 -0
  17. pypproxy-0.3.0/pypproxy/frida/pinning_bypass.py +285 -0
  18. pypproxy-0.3.0/pypproxy/macro/runner.py +193 -0
  19. pypproxy-0.3.0/pypproxy/openapi/generator.py +248 -0
  20. pypproxy-0.3.0/pypproxy/report/generator.py +148 -0
  21. pypproxy-0.3.0/pypproxy/rule/__init__.py +0 -0
  22. pypproxy-0.3.0/pypproxy/scan/__init__.py +0 -0
  23. pypproxy-0.3.0/pypproxy/script/__init__.py +0 -0
  24. pypproxy-0.3.0/pypproxy/security/__init__.py +0 -0
  25. pypproxy-0.3.0/pypproxy/security/advanced_checks.py +296 -0
  26. pypproxy-0.3.0/pypproxy/security/idor.py +207 -0
  27. pypproxy-0.3.0/pypproxy/session/__init__.py +0 -0
  28. pypproxy-0.3.0/pypproxy/session/manager.py +139 -0
  29. pypproxy-0.3.0/pypproxy/store/__init__.py +0 -0
  30. pypproxy-0.3.0/pypproxy/ui/__init__.py +0 -0
  31. pypproxy-0.3.0/pypproxy/ui/ab_tab.py +116 -0
  32. pypproxy-0.3.0/pypproxy/ui/advanced_security_tab.py +277 -0
  33. pypproxy-0.3.0/pypproxy/ui/analytics_tab.py +173 -0
  34. pypproxy-0.3.0/pypproxy/ui/app.py +582 -0
  35. pypproxy-0.3.0/pypproxy/ui/codegen_tab.py +67 -0
  36. pypproxy-0.3.0/pypproxy/ui/detail.py +293 -0
  37. pypproxy-0.3.0/pypproxy/ui/frida_tab.py +433 -0
  38. pypproxy-0.3.0/pypproxy/ui/macro_tab.py +171 -0
  39. pypproxy-0.3.0/pypproxy/ui/openapi_tab.py +77 -0
  40. pypproxy-0.3.0/pypproxy/ui/report_tab.py +75 -0
  41. pypproxy-0.3.0/pypproxy/ui/session_tab.py +95 -0
  42. pypproxy-0.3.0/pypproxy/ui/theme.py +375 -0
  43. {pypproxy-0.1.0 → pypproxy-0.3.0}/pyproject.toml +4 -1
  44. pypproxy-0.3.0/tests/__init__.py +0 -0
  45. pypproxy-0.3.0/tests/test_advanced_tools.py +254 -0
  46. pypproxy-0.3.0/tests/test_all_features.py +306 -0
  47. pypproxy-0.3.0/tests/test_decode.py +261 -0
  48. pypproxy-0.3.0/tests/test_frida.py +231 -0
  49. {pypproxy-0.1.0 → pypproxy-0.3.0}/uv.lock +29 -1
  50. pypproxy-0.1.0/pypproxy/codec.py +0 -176
  51. pypproxy-0.1.0/pypproxy/ui/app.py +0 -386
  52. pypproxy-0.1.0/pypproxy/ui/detail.py +0 -179
  53. pypproxy-0.1.0/pypproxy/ui/theme.py +0 -59
  54. {pypproxy-0.1.0 → pypproxy-0.3.0}/.github/dependabot.yml +0 -0
  55. {pypproxy-0.1.0 → pypproxy-0.3.0}/.github/workflows/ci.yml +0 -0
  56. {pypproxy-0.1.0 → pypproxy-0.3.0}/.github/workflows/docs.yml +0 -0
  57. {pypproxy-0.1.0 → pypproxy-0.3.0}/.github/workflows/publish.yml +0 -0
  58. {pypproxy-0.1.0 → pypproxy-0.3.0}/.gitignore +0 -0
  59. {pypproxy-0.1.0 → pypproxy-0.3.0}/.pre-commit-config.yaml +0 -0
  60. {pypproxy-0.1.0 → pypproxy-0.3.0}/LICENSE +0 -0
  61. {pypproxy-0.1.0 → pypproxy-0.3.0}/Makefile +0 -0
  62. {pypproxy-0.1.0 → pypproxy-0.3.0}/README.md +0 -0
  63. {pypproxy-0.1.0 → pypproxy-0.3.0}/docs/replay.md +0 -0
  64. {pypproxy-0.1.0 → pypproxy-0.3.0}/docs/rule-engine.md +0 -0
  65. {pypproxy-0.1.0 → pypproxy-0.3.0}/main.py +0 -0
  66. {pypproxy-0.1.0 → pypproxy-0.3.0}/mkdocs.yml +0 -0
  67. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/__init__.py +0 -0
  68. {pypproxy-0.1.0/pypproxy/api → pypproxy-0.3.0/pypproxy/ab_test}/__init__.py +0 -0
  69. {pypproxy-0.1.0/pypproxy/bulk → pypproxy-0.3.0/pypproxy/analytics}/__init__.py +0 -0
  70. {pypproxy-0.1.0/pypproxy/cert → pypproxy-0.3.0/pypproxy/api}/__init__.py +0 -0
  71. {pypproxy-0.1.0/pypproxy/config → pypproxy-0.3.0/pypproxy/bulk}/__init__.py +0 -0
  72. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/bulk/sender.py +0 -0
  73. {pypproxy-0.1.0/pypproxy/dns → pypproxy-0.3.0/pypproxy/cert}/__init__.py +0 -0
  74. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/cert/ca.py +0 -0
  75. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/cert/client_cert.py +0 -0
  76. {pypproxy-0.1.0/pypproxy/exporter → pypproxy-0.3.0/pypproxy/codegen}/__init__.py +0 -0
  77. {pypproxy-0.1.0/pypproxy/graphql → pypproxy-0.3.0/pypproxy/config}/__init__.py +0 -0
  78. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/config/config.py +0 -0
  79. {pypproxy-0.1.0/pypproxy/intercept → pypproxy-0.3.0/pypproxy/dns}/__init__.py +0 -0
  80. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/dns/server.py +0 -0
  81. {pypproxy-0.1.0/pypproxy/interceptor → pypproxy-0.3.0/pypproxy/exporter}/__init__.py +0 -0
  82. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/exporter/exporter.py +0 -0
  83. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/exporter/importer.py +0 -0
  84. {pypproxy-0.1.0/pypproxy/proto → pypproxy-0.3.0/pypproxy/frida}/__init__.py +0 -0
  85. {pypproxy-0.1.0/pypproxy/proxy → pypproxy-0.3.0/pypproxy/graphql}/__init__.py +0 -0
  86. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/graphql/detector.py +0 -0
  87. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/graphql/introspection.py +0 -0
  88. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/graphql/modifier.py +0 -0
  89. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/graphql/schema_store.py +0 -0
  90. {pypproxy-0.1.0/pypproxy/replay → pypproxy-0.3.0/pypproxy/intercept}/__init__.py +0 -0
  91. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/intercept/manager.py +0 -0
  92. {pypproxy-0.1.0/pypproxy/rule → pypproxy-0.3.0/pypproxy/interceptor}/__init__.py +0 -0
  93. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/interceptor/interceptor.py +0 -0
  94. {pypproxy-0.1.0/pypproxy/scan → pypproxy-0.3.0/pypproxy/macro}/__init__.py +0 -0
  95. {pypproxy-0.1.0/pypproxy/script → pypproxy-0.3.0/pypproxy/openapi}/__init__.py +0 -0
  96. {pypproxy-0.1.0/pypproxy/security → pypproxy-0.3.0/pypproxy/proto}/__init__.py +0 -0
  97. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/proto/grpc.py +0 -0
  98. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/proto/mqtt.py +0 -0
  99. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/proto/ws.py +0 -0
  100. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/proto/ws_intercept.py +0 -0
  101. {pypproxy-0.1.0/pypproxy/store → pypproxy-0.3.0/pypproxy/proxy}/__init__.py +0 -0
  102. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/proxy/proxy.py +0 -0
  103. {pypproxy-0.1.0/pypproxy/ui → pypproxy-0.3.0/pypproxy/replay}/__init__.py +0 -0
  104. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/replay/replay.py +0 -0
  105. {pypproxy-0.1.0/tests → pypproxy-0.3.0/pypproxy/report}/__init__.py +0 -0
  106. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/rule/rule.py +0 -0
  107. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/scan/scanner.py +0 -0
  108. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/script/engine.py +0 -0
  109. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/security/header_checker.py +0 -0
  110. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/security/int_overflow.py +0 -0
  111. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/security/jwt_checker.py +0 -0
  112. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/security/plugin.py +0 -0
  113. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/security/randomness.py +0 -0
  114. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/store/db.py +0 -0
  115. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/store/filter_parser.py +0 -0
  116. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/store/fts.py +0 -0
  117. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/store/models.py +0 -0
  118. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/store/scope.py +0 -0
  119. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/store/store.py +0 -0
  120. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/ui/bulk_sender_ui.py +0 -0
  121. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/ui/cui.py +0 -0
  122. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/ui/diff_view.py +0 -0
  123. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/ui/graphql_tab.py +0 -0
  124. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/ui/import_tab.py +0 -0
  125. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/ui/intercept_dialog.py +0 -0
  126. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/ui/resender.py +0 -0
  127. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/ui/scan_tab.py +0 -0
  128. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/ui/security_tab.py +0 -0
  129. {pypproxy-0.1.0 → pypproxy-0.3.0}/pypproxy/ui/settings.py +0 -0
  130. {pypproxy-0.1.0 → pypproxy-0.3.0}/tests/test_advanced.py +0 -0
  131. {pypproxy-0.1.0 → pypproxy-0.3.0}/tests/test_bulk.py +0 -0
  132. {pypproxy-0.1.0 → pypproxy-0.3.0}/tests/test_cert.py +0 -0
  133. {pypproxy-0.1.0 → pypproxy-0.3.0}/tests/test_codec.py +0 -0
  134. {pypproxy-0.1.0 → pypproxy-0.3.0}/tests/test_exporter.py +0 -0
  135. {pypproxy-0.1.0 → pypproxy-0.3.0}/tests/test_filter_parser.py +0 -0
  136. {pypproxy-0.1.0 → pypproxy-0.3.0}/tests/test_graphql.py +0 -0
  137. {pypproxy-0.1.0 → pypproxy-0.3.0}/tests/test_interceptor.py +0 -0
  138. {pypproxy-0.1.0 → pypproxy-0.3.0}/tests/test_mqtt.py +0 -0
  139. {pypproxy-0.1.0 → pypproxy-0.3.0}/tests/test_rule.py +0 -0
  140. {pypproxy-0.1.0 → pypproxy-0.3.0}/tests/test_security.py +0 -0
  141. {pypproxy-0.1.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.1.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
@@ -17,3 +17,5 @@ Requires-Dist: pyyaml>=6.0.3
17
17
  Requires-Dist: rich>=15.0.0
18
18
  Requires-Dist: uvicorn[standard]>=0.30.0
19
19
  Requires-Dist: websockets>=16.0
20
+ Provides-Extra: frida
21
+ Requires-Dist: frida>=16.0.0; extra == 'frida'
@@ -63,7 +63,7 @@ CRUD for intercept rules.
63
63
  | `GET /api/export/json` | Export all entries + rules as JSON |
64
64
  | `GET /api/export/har` | Export all entries as HAR 1.2 |
65
65
  | `POST /api/import/har` | Import entries from HAR body |
66
- | `POST /api/import/json` | Import entries from paxy JSON body |
66
+ | `POST /api/import/json` | Import entries from pypproxy JSON body |
67
67
  | `POST /api/import/rules` | Import rules from JSON body |
68
68
 
69
69
  ---
@@ -8,24 +8,24 @@
8
8
  └────────────────────────┬─────────────────────────────────────┘
9
9
  │ HTTP / CONNECT
10
10
  ┌────────────────────────▼─────────────────────────────────────┐
11
- paxy/proxy/proxy.py (port :8080) │
11
+ pypproxy/proxy/proxy.py (port :8080) │
12
12
  │ asyncio TCP server │
13
13
  │ ├─ HTTP → intercept → forward upstream (httpx, HTTP/2) │
14
14
  │ ├─ CONNECT → TLS termination (MITM) │
15
15
  │ │ ├─ HTTP/HTTPS → intercept → forward upstream │
16
- │ │ ├─ WebSocket → paxy/proto/ws.py │
17
- │ │ ├─ gRPC → paxy/proto/grpc.py │
18
- │ │ └─ MQTT → paxy/proto/mqtt.py │
16
+ │ │ ├─ WebSocket → pypproxy/proto/ws.py │
17
+ │ │ ├─ gRPC → pypproxy/proto/grpc.py │
18
+ │ │ └─ MQTT → pypproxy/proto/mqtt.py │
19
19
  │ └─ ignored hosts → raw TCP tunnel (passthrough) │
20
20
  └─────────────┬────────────────────────┬───────────────────────┘
21
21
  │ │
22
22
  ┌─────────────▼──────┐ ┌────────────▼──────────────────────┐
23
- paxy/cert/ca.py │ │ paxy/interceptor/ │
23
+ pypproxy/cert/ca.py │ │ pypproxy/interceptor/ │
24
24
  │ CA + per-host TLS │ │ apply rules, record entries │
25
25
  │ SSL Context cache │ └────────────┬───────────────────────┘
26
26
  │ │ │
27
- paxy/cert/ │ ┌────────────▼───────────────────────┐
28
- │ client_cert.py │ │ paxy/store/store.py │
27
+ pypproxy/cert/ │ ┌────────────▼───────────────────────┐
28
+ │ client_cert.py │ │ pypproxy/store/store.py │
29
29
  │ Mutual TLS certs │ │ in-memory store + SQLite persist │
30
30
  └────────────────────┘ │ asyncio pub/sub │
31
31
  └────────────┬───────────────────────┘
@@ -33,7 +33,7 @@
33
33
  ┌────────────────────────────┼──────────────────────┐
34
34
  │ │ │
35
35
  ┌─────────▼──────────┐ ┌─────────────▼─────────┐ ┌────────▼──────────┐
36
- paxy/api/ │ │ paxy/ui/app.py │ │ paxy/ui/cui.py │
36
+ pypproxy/api/ │ │ pypproxy/ui/app.py │ │ pypproxy/ui/cui.py │
37
37
  │ FastAPI REST API │ │ NiceGUI 4-tab UI │ │ rich terminal UI │
38
38
  │ + WebSocket /ws │ │ Traffic/Resender/ │ │ (CUI mode) │
39
39
  │ Bulk/Export APIs │ │ Bulk/Diff │ └───────────────────┘
@@ -44,34 +44,34 @@
44
44
 
45
45
  | Package | Responsibility |
46
46
  |---------|----------------|
47
- | `paxy/proxy` | asyncio TCP server; HTTP forwarding; TLS MITM for CONNECT; raw tunnel for ignored hosts |
48
- | `paxy/cert/ca` | CA certificate generation; per-host SSL Context cache |
49
- | `paxy/cert/client_cert` | Client certificate management for mutual TLS |
50
- | `paxy/interceptor` | Apply rules to requests and responses; record entries in the store |
51
- | `paxy/intercept` | Manual intercept manager; pause requests for user review |
52
- | `paxy/rule` | Rule evaluation engine; condition matching; priority ordering |
53
- | `paxy/store/store` | Thread-safe in-memory traffic store; asyncio pub/sub |
54
- | `paxy/store/db` | SQLite persistence via aiosqlite; load/save entries |
55
- | `paxy/store/filter_parser` | Filter expression parser (`host == x && method == POST`) |
56
- | `paxy/api` | FastAPI REST endpoints, WebSocket streaming, bulk/export APIs |
57
- | `paxy/ui/app` | NiceGUI 4-tab browser UI (Traffic, Resender, Bulk Sender, Diff) |
58
- | `paxy/ui/settings` | Settings page (rules, SSL passthrough, DNS, ports, client certs) |
59
- | `paxy/ui/detail` | Request/response detail panel with body view selector |
60
- | `paxy/ui/resender` | Resender tab — edit and re-send requests |
61
- | `paxy/ui/bulk_sender_ui` | Bulk Sender tab — parallel payload sending and race testing |
62
- | `paxy/ui/diff_view` | Diff tab — unified diff between two captured entries |
63
- | `paxy/ui/intercept_dialog` | Intercept dialog — pause, edit, forward or drop requests |
64
- | `paxy/ui/cui` | rich terminal UI (CUI mode) |
65
- | `paxy/proto/ws` | WebSocket frame relay and logging |
66
- | `paxy/proto/grpc` | gRPC length-prefix frame decoding |
67
- | `paxy/proto/mqtt` | MQTT frame decoding and detection |
68
- | `paxy/script` | Python script engine; `on_request` / `on_response` hooks |
69
- | `paxy/replay` | Async HTTP replay and parallel fuzzing via httpx |
70
- | `paxy/bulk` | Bulk sender and race condition test runner |
71
- | `paxy/dns` | Built-in DNS server with domain spoofing |
72
- | `paxy/exporter` | JSON/HAR export and rule import/export |
73
- | `paxy/codec` | Content-encoding decode (gzip/br/deflate); binary format decode (Protobuf/MessagePack/CBOR) |
74
- | `paxy/config` | YAML config loading |
47
+ | `pypproxy/proxy` | asyncio TCP server; HTTP forwarding; TLS MITM for CONNECT; raw tunnel for ignored hosts |
48
+ | `pypproxy/cert/ca` | CA certificate generation; per-host SSL Context cache |
49
+ | `pypproxy/cert/client_cert` | Client certificate management for mutual TLS |
50
+ | `pypproxy/interceptor` | Apply rules to requests and responses; record entries in the store |
51
+ | `pypproxy/intercept` | Manual intercept manager; pause requests for user review |
52
+ | `pypproxy/rule` | Rule evaluation engine; condition matching; priority ordering |
53
+ | `pypproxy/store/store` | Thread-safe in-memory traffic store; asyncio pub/sub |
54
+ | `pypproxy/store/db` | SQLite persistence via aiosqlite; load/save entries |
55
+ | `pypproxy/store/filter_parser` | Filter expression parser (`host == x && method == POST`) |
56
+ | `pypproxy/api` | FastAPI REST endpoints, WebSocket streaming, bulk/export APIs |
57
+ | `pypproxy/ui/app` | NiceGUI 4-tab browser UI (Traffic, Resender, Bulk Sender, Diff) |
58
+ | `pypproxy/ui/settings` | Settings page (rules, SSL passthrough, DNS, ports, client certs) |
59
+ | `pypproxy/ui/detail` | Request/response detail panel with body view selector |
60
+ | `pypproxy/ui/resender` | Resender tab — edit and re-send requests |
61
+ | `pypproxy/ui/bulk_sender_ui` | Bulk Sender tab — parallel payload sending and race testing |
62
+ | `pypproxy/ui/diff_view` | Diff tab — unified diff between two captured entries |
63
+ | `pypproxy/ui/intercept_dialog` | Intercept dialog — pause, edit, forward or drop requests |
64
+ | `pypproxy/ui/cui` | rich terminal UI (CUI mode) |
65
+ | `pypproxy/proto/ws` | WebSocket frame relay and logging |
66
+ | `pypproxy/proto/grpc` | gRPC length-prefix frame decoding |
67
+ | `pypproxy/proto/mqtt` | MQTT frame decoding and detection |
68
+ | `pypproxy/script` | Python script engine; `on_request` / `on_response` hooks |
69
+ | `pypproxy/replay` | Async HTTP replay and parallel fuzzing via httpx |
70
+ | `pypproxy/bulk` | Bulk sender and race condition test runner |
71
+ | `pypproxy/dns` | Built-in DNS server with domain spoofing |
72
+ | `pypproxy/exporter` | JSON/HAR export and rule import/export |
73
+ | `pypproxy/codec` | Content-encoding decode (gzip/br/deflate); binary format decode (Protobuf/MessagePack/CBOR) |
74
+ | `pypproxy/config` | YAML config loading |
75
75
 
76
76
  ## Key design decisions
77
77
 
@@ -82,11 +82,11 @@ This lets a single connection handle HTTP/1.1 keep-alive, CONNECT tunnels, WebSo
82
82
 
83
83
  ### TLS termination with `loop.start_tls()`
84
84
 
85
- After responding `200 Connection Established` to a CONNECT request, paxy calls `loop.start_tls()` to upgrade the existing asyncio transport to TLS server-side. Per-host certificates are cached as `ssl.SSLContext` objects.
85
+ After responding `200 Connection Established` to a CONNECT request, pypproxy calls `loop.start_tls()` to upgrade the existing asyncio transport to TLS server-side. Per-host certificates are cached as `ssl.SSLContext` objects.
86
86
 
87
87
  ### SQLite persistence via aiosqlite
88
88
 
89
- All captured traffic is stored in memory for fast access. Writes to SQLite are fire-and-forget via `asyncio.run_coroutine_threadsafe`. On startup, `store.load_from_db()` restores prior sessions. The DB path defaults to `~/.paxy/paxy.db`.
89
+ All captured traffic is stored in memory for fast access. Writes to SQLite are fire-and-forget via `asyncio.run_coroutine_threadsafe`. On startup, `store.load_from_db()` restores prior sessions. The DB path defaults to `~/.pypproxy/pypproxy.db`.
90
90
 
91
91
  ### Store pub/sub
92
92
 
@@ -98,11 +98,11 @@ All upstream requests use `httpx` with `http2=True`. httpx negotiates HTTP/2 via
98
98
 
99
99
  ### Filter expression engine
100
100
 
101
- The filter bar in the UI accepts a structured expression parsed by `paxy/store/filter_parser.py`. The parser tokenizes `field op value` conditions and evaluates them with AND/OR short-circuit logic against `Entry` objects in memory.
101
+ The filter bar in the UI accepts a structured expression parsed by `pypproxy/store/filter_parser.py`. The parser tokenizes `field op value` conditions and evaluates them with AND/OR short-circuit logic against `Entry` objects in memory.
102
102
 
103
103
  ### Binary format detection
104
104
 
105
- `paxy/codec.py` implements `sniff_content_type()` which combines Content-Type inspection with a JSON parse attempt and a binary entropy heuristic to guess the best display mode. Protobuf decoding uses wire-type heuristics without requiring a `.proto` schema.
105
+ `pypproxy/codec.py` implements `sniff_content_type()` which combines Content-Type inspection with a JSON parse attempt and a binary entropy heuristic to guess the best display mode. Protobuf decoding uses wire-type heuristics without requiring a `.proto` schema.
106
106
 
107
107
  ### GUI / CUI startup
108
108
 
@@ -5,7 +5,7 @@ paxy can be configured via CLI flags or a YAML file. CLI flags take precedence o
5
5
  ## YAML config file
6
6
 
7
7
  ```bash
8
- uv run python main.py --config paxy.yaml
8
+ uv run python main.py --config pypproxy.yaml
9
9
  ```
10
10
 
11
11
  ### Full example
@@ -9,7 +9,7 @@
9
9
 
10
10
  ```bash
11
11
  git clone https://github.com/ykus4/pypproxy
12
- cd paxy
12
+ cd pypproxy
13
13
  uv sync
14
14
  ```
15
15
 
@@ -2,7 +2,7 @@
2
2
  layout: home
3
3
 
4
4
  hero:
5
- name: paxy
5
+ name: pypproxy
6
6
  text: MITM Proxy
7
7
  tagline: Intercept, inspect, and modify HTTP/HTTPS traffic from browsers and mobile apps. Written in Python.
8
8
  actions:
@@ -21,7 +21,7 @@ Open the **GraphQL** tab or right-click a GraphQL entry → **Open in GraphQL ta
21
21
 
22
22
  **Schema Introspection**
23
23
 
24
- Enter the endpoint URL and click **Introspect**. paxy sends a full `__schema` introspection query and displays the type tree. The schema is cached per-host and available for query completion.
24
+ Enter the endpoint URL and click **Introspect**. pypproxy sends a full `__schema` introspection query and displays the type tree. The schema is cached per-host and available for query completion.
25
25
 
26
26
  **Query Editor**
27
27
 
@@ -44,7 +44,7 @@ Shows the operation type (query/mutation/subscription), operation name, and top-
44
44
  ### Modifier utilities (`paxy.graphql.modifier`)
45
45
 
46
46
  ```python
47
- from paxy.graphql.modifier import set_variable, build_query, build_mutation
47
+ from pypproxy.graphql.modifier import set_variable, build_query, build_mutation
48
48
 
49
49
  # Replace a variable in a captured request body
50
50
  new_body = set_variable(entry.req_body, "userId", "456")
@@ -65,8 +65,8 @@ WebSocket connections are detected automatically and intercepted as part of the
65
65
  ### How it works
66
66
 
67
67
  1. The client sends a CONNECT request to the proxy.
68
- 2. paxy terminates TLS (same as HTTPS MITM).
69
- 3. When paxy sees `Upgrade: websocket` in the decrypted stream, it switches to WebSocket relay mode.
68
+ 2. pypproxy terminates TLS (same as HTTPS MITM).
69
+ 3. When pypproxy sees `Upgrade: websocket` in the decrypted stream, it switches to WebSocket relay mode.
70
70
  4. Frames are relayed between client and server while being logged.
71
71
 
72
72
  ### Frame intercept
@@ -106,7 +106,7 @@ All upstream requests use `httpx` with HTTP/2 support enabled. Connections negot
106
106
 
107
107
  ## Certificate pinning
108
108
 
109
- Add pinned hosts to the `ignore` list in config or the **SSL Passthrough** settings tab. paxy tunnels those hosts without TLS interception.
109
+ Add pinned hosts to the `ignore` list in config or the **SSL Passthrough** settings tab. pypproxy tunnels those hosts without TLS interception.
110
110
 
111
111
  ```yaml
112
112
  proxy:
@@ -17,7 +17,7 @@ script:
17
17
 
18
18
  ## Hook functions
19
19
 
20
- Define any of these functions in your script. paxy calls them automatically.
20
+ Define any of these functions in your script. pypproxy calls them automatically.
21
21
 
22
22
  ### `on_request(method, host, path, body)`
23
23
 
@@ -90,7 +90,7 @@ def on_response(status: int, body: bytes) -> bytes:
90
90
 
91
91
  ## Notes
92
92
 
93
- - The script is loaded once at startup. Restart paxy to pick up changes.
93
+ - The script is loaded once at startup. Restart pypproxy to pick up changes.
94
94
  - Both hooks may be called concurrently from multiple requests. Protect shared state with a lock if needed.
95
- - Use `sys.stderr` for logging to avoid mixing output with paxy's own logs.
95
+ - Use `sys.stderr` for logging to avoid mixing output with pypproxy's own logs.
96
96
  - The full Python standard library and any packages in the project's virtualenv are available.
@@ -2,14 +2,14 @@
2
2
 
3
3
  ## GUI Mode
4
4
 
5
- Start paxy in GUI mode (the default) and open `http://localhost:8081`.
5
+ Start pypproxy in GUI mode (the default) and open `http://localhost:8081`.
6
6
  The UI is built with [NiceGUI](https://nicegui.io/) and runs in the same Python process as the proxy.
7
7
 
8
8
  ## Layout
9
9
 
10
10
  ```
11
11
  ┌──────────────────────────────────────────────────────────────┐
12
- │ toolbar: paxy ● | Filter expression | Intercept | Clear | ⚙ │
12
+ │ toolbar: pypproxy ● | Filter expression | Intercept | Clear | ⚙ │
13
13
  ├─────────────────────────────────────────────────────────────-┤
14
14
  │ Traffic | Resender | Bulk Sender | Diff │
15
15
  ├──────────────────────────┬───────────────────────────────────┤
@@ -0,0 +1,136 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import time
5
+ from dataclasses import dataclass
6
+
7
+ import httpx
8
+
9
+ from pypproxy.store.models import Entry
10
+
11
+
12
+ @dataclass
13
+ class ABResult:
14
+ endpoint_a: str
15
+ endpoint_b: str
16
+ method: str
17
+ status_a: int
18
+ status_b: int
19
+ body_a: bytes
20
+ body_b: bytes
21
+ duration_a_ms: int
22
+ duration_b_ms: int
23
+ error_a: str = ""
24
+ error_b: str = ""
25
+
26
+ @property
27
+ def status_diff(self) -> bool:
28
+ return self.status_a != self.status_b
29
+
30
+ @property
31
+ def body_diff(self) -> bool:
32
+ return self.body_a != self.body_b
33
+
34
+ def to_dict(self) -> dict:
35
+ import base64
36
+
37
+ return {
38
+ "endpoint_a": self.endpoint_a,
39
+ "endpoint_b": self.endpoint_b,
40
+ "method": self.method,
41
+ "status_a": self.status_a,
42
+ "status_b": self.status_b,
43
+ "body_a": base64.b64encode(self.body_a).decode() if self.body_a else "",
44
+ "body_b": base64.b64encode(self.body_b).decode() if self.body_b else "",
45
+ "duration_a_ms": self.duration_a_ms,
46
+ "duration_b_ms": self.duration_b_ms,
47
+ "error_a": self.error_a,
48
+ "error_b": self.error_b,
49
+ "status_diff": self.status_diff,
50
+ "body_diff": self.body_diff,
51
+ }
52
+
53
+ def diff_summary(self) -> str:
54
+ lines: list[str] = []
55
+ if self.status_diff:
56
+ lines.append(f"Status: A={self.status_a} B={self.status_b}")
57
+ else:
58
+ lines.append(f"Status: both {self.status_a}")
59
+ if self.body_diff:
60
+ lines.append(f"Body differs ({len(self.body_a):,} B vs {len(self.body_b):,} B)")
61
+ # Try JSON diff summary
62
+ try:
63
+ da = json.loads(self.body_a)
64
+ db = json.loads(self.body_b)
65
+ if isinstance(da, dict) and isinstance(db, dict):
66
+ added = set(db) - set(da)
67
+ removed = set(da) - set(db)
68
+ changed = {k for k in da if k in db and da[k] != db[k]}
69
+ if added:
70
+ lines.append(f" + fields added: {', '.join(sorted(added)[:5])}")
71
+ if removed:
72
+ lines.append(f" - fields removed: {', '.join(sorted(removed)[:5])}")
73
+ if changed:
74
+ lines.append(f" ~ fields changed: {', '.join(sorted(changed)[:5])}")
75
+ except Exception:
76
+ pass
77
+ else:
78
+ lines.append("Body: identical")
79
+ lines.append(f"Latency: A={self.duration_a_ms}ms B={self.duration_b_ms}ms")
80
+ return "\n".join(lines)
81
+
82
+
83
+ async def run_ab_test(
84
+ entry: Entry,
85
+ override_host_b: str,
86
+ override_scheme_b: str = "",
87
+ timeout: int = 30,
88
+ ) -> ABResult:
89
+ """Send the same request to two different hosts and compare responses."""
90
+ scheme = entry.scheme
91
+ path = entry.path
92
+ query = entry.query
93
+ headers = {
94
+ k: ", ".join(v)
95
+ for k, v in entry.req_headers.items()
96
+ if k.lower() not in ("host", "content-length", "connection")
97
+ }
98
+ body = entry.req_body
99
+
100
+ url_a = f"{scheme}://{entry.host}{path}" + (f"?{query}" if query else "")
101
+ scheme_b = override_scheme_b or scheme
102
+ url_b = f"{scheme_b}://{override_host_b}{path}" + (f"?{query}" if query else "")
103
+
104
+ status_a = status_b = 0
105
+ body_a = body_b = b""
106
+ dur_a = dur_b = 0
107
+ err_a = err_b = ""
108
+
109
+ async def _fetch(url: str) -> tuple[int, bytes, int, str]:
110
+ start = time.monotonic()
111
+ try:
112
+ h = dict(headers)
113
+ h["host"] = url.split("/")[2].split(":")[0]
114
+ async with httpx.AsyncClient(verify=False, timeout=timeout, http2=True) as client:
115
+ resp = await client.request(method=entry.method, url=url, headers=h, content=body)
116
+ return resp.status_code, resp.content, int((time.monotonic() - start) * 1000), ""
117
+ except Exception as e:
118
+ return 0, b"", int((time.monotonic() - start) * 1000), str(e)
119
+
120
+ (status_a, body_a, dur_a, err_a), (status_b, body_b, dur_b, err_b) = await __import__(
121
+ "asyncio"
122
+ ).gather(_fetch(url_a), _fetch(url_b))
123
+
124
+ return ABResult(
125
+ endpoint_a=url_a,
126
+ endpoint_b=url_b,
127
+ method=entry.method,
128
+ status_a=status_a,
129
+ status_b=status_b,
130
+ body_a=body_a,
131
+ body_b=body_b,
132
+ duration_a_ms=dur_a,
133
+ duration_b_ms=dur_b,
134
+ error_a=err_a,
135
+ error_b=err_b,
136
+ )
@@ -0,0 +1,225 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import Counter, defaultdict
4
+ from dataclasses import dataclass
5
+ from typing import Any
6
+
7
+ from pypproxy.store.models import Entry, Filter
8
+ from pypproxy.store.store import Store
9
+
10
+
11
+ @dataclass
12
+ class HostStats:
13
+ host: str
14
+ count: int
15
+ methods: dict[str, int]
16
+ status_codes: dict[int, int]
17
+ avg_duration_ms: float
18
+ max_duration_ms: int
19
+ error_rate: float # 4xx+5xx / total
20
+ protocols: dict[str, int]
21
+
22
+
23
+ @dataclass
24
+ class EndpointStats:
25
+ method: str
26
+ path: str
27
+ count: int
28
+ avg_duration_ms: float
29
+ status_codes: dict[int, int]
30
+ error_rate: float
31
+
32
+
33
+ @dataclass
34
+ class TrafficSummary:
35
+ total: int
36
+ hosts: list[HostStats]
37
+ top_endpoints: list[EndpointStats]
38
+ status_distribution: dict[str, int] # "2xx", "3xx", "4xx", "5xx"
39
+ method_distribution: dict[str, int]
40
+ protocol_distribution: dict[str, int]
41
+ avg_duration_ms: float
42
+ p95_duration_ms: int
43
+ p99_duration_ms: int
44
+ errors: list[dict] # top 5xx/4xx entries
45
+
46
+ def to_dict(self) -> dict[str, Any]:
47
+ return {
48
+ "total": self.total,
49
+ "hosts": [
50
+ {
51
+ "host": h.host,
52
+ "count": h.count,
53
+ "methods": h.methods,
54
+ "status_codes": {str(k): v for k, v in h.status_codes.items()},
55
+ "avg_duration_ms": round(h.avg_duration_ms, 1),
56
+ "max_duration_ms": h.max_duration_ms,
57
+ "error_rate": round(h.error_rate, 3),
58
+ "protocols": h.protocols,
59
+ }
60
+ for h in self.hosts
61
+ ],
62
+ "top_endpoints": [
63
+ {
64
+ "method": e.method,
65
+ "path": e.path,
66
+ "count": e.count,
67
+ "avg_duration_ms": round(e.avg_duration_ms, 1),
68
+ "status_codes": {str(k): v for k, v in e.status_codes.items()},
69
+ "error_rate": round(e.error_rate, 3),
70
+ }
71
+ for e in self.top_endpoints
72
+ ],
73
+ "status_distribution": self.status_distribution,
74
+ "method_distribution": self.method_distribution,
75
+ "protocol_distribution": self.protocol_distribution,
76
+ "avg_duration_ms": round(self.avg_duration_ms, 1),
77
+ "p95_duration_ms": self.p95_duration_ms,
78
+ "p99_duration_ms": self.p99_duration_ms,
79
+ "errors": self.errors,
80
+ }
81
+
82
+
83
+ def compute(store: Store, f: Filter | None = None) -> TrafficSummary:
84
+ entries, _ = store.list(f or Filter(), 0, 0)
85
+ return compute_from_entries(entries)
86
+
87
+
88
+ def compute_from_entries(entries: list[Entry]) -> TrafficSummary:
89
+ if not entries:
90
+ return TrafficSummary(
91
+ total=0,
92
+ hosts=[],
93
+ top_endpoints=[],
94
+ status_distribution={},
95
+ method_distribution={},
96
+ protocol_distribution={},
97
+ avg_duration_ms=0,
98
+ p95_duration_ms=0,
99
+ p99_duration_ms=0,
100
+ errors=[],
101
+ )
102
+
103
+ # Per-host aggregation
104
+ host_data: dict[str, dict] = defaultdict(
105
+ lambda: {
106
+ "count": 0,
107
+ "methods": Counter(),
108
+ "status_codes": Counter(),
109
+ "durations": [],
110
+ "protocols": Counter(),
111
+ }
112
+ )
113
+ endpoint_data: dict[tuple[str, str], dict] = defaultdict(
114
+ lambda: {
115
+ "count": 0,
116
+ "durations": [],
117
+ "status_codes": Counter(),
118
+ }
119
+ )
120
+ status_dist: Counter = Counter()
121
+ method_dist: Counter = Counter()
122
+ proto_dist: Counter = Counter()
123
+ all_durations: list[int] = []
124
+ error_entries: list[Entry] = []
125
+
126
+ for e in entries:
127
+ h = host_data[e.host]
128
+ h["count"] += 1
129
+ h["methods"][e.method] += 1
130
+ if e.status_code:
131
+ h["status_codes"][e.status_code] += 1
132
+ if e.duration_ms:
133
+ h["durations"].append(e.duration_ms)
134
+ all_durations.append(e.duration_ms)
135
+ h["protocols"][e.protocol] += 1
136
+
137
+ ep_key = (e.method, e.path)
138
+ ep = endpoint_data[ep_key]
139
+ ep["count"] += 1
140
+ if e.duration_ms:
141
+ ep["durations"].append(e.duration_ms)
142
+ if e.status_code:
143
+ ep["status_codes"][e.status_code] += 1
144
+
145
+ if e.status_code:
146
+ bucket = f"{e.status_code // 100}xx"
147
+ status_dist[bucket] += 1
148
+ if e.status_code >= 400:
149
+ error_entries.append(e)
150
+
151
+ method_dist[e.method] += 1
152
+ proto_dist[e.protocol] += 1
153
+
154
+ # Build host stats
155
+ hosts = []
156
+ for host, d in sorted(host_data.items(), key=lambda x: -x[1]["count"])[:20]:
157
+ total_h = d["count"]
158
+ errors_h = sum(v for k, v in d["status_codes"].items() if k >= 400)
159
+ durations = d["durations"]
160
+ hosts.append(
161
+ HostStats(
162
+ host=host,
163
+ count=total_h,
164
+ methods=dict(d["methods"]),
165
+ status_codes=dict(d["status_codes"]),
166
+ avg_duration_ms=sum(durations) / len(durations) if durations else 0,
167
+ max_duration_ms=max(durations) if durations else 0,
168
+ error_rate=errors_h / total_h if total_h else 0,
169
+ protocols=dict(d["protocols"]),
170
+ )
171
+ )
172
+
173
+ # Build endpoint stats
174
+ endpoints = []
175
+ for (method, path), d in sorted(endpoint_data.items(), key=lambda x: -x[1]["count"])[:20]:
176
+ total_ep = d["count"]
177
+ errors_ep = sum(v for k, v in d["status_codes"].items() if k >= 400)
178
+ durations = d["durations"]
179
+ endpoints.append(
180
+ EndpointStats(
181
+ method=method,
182
+ path=path,
183
+ count=total_ep,
184
+ avg_duration_ms=sum(durations) / len(durations) if durations else 0,
185
+ status_codes=dict(d["status_codes"]),
186
+ error_rate=errors_ep / total_ep if total_ep else 0,
187
+ )
188
+ )
189
+
190
+ # Percentiles
191
+ sorted_dur = sorted(all_durations)
192
+ n = len(sorted_dur)
193
+ p95 = sorted_dur[int(n * 0.95)] if n > 0 else 0
194
+ p99 = sorted_dur[int(n * 0.99)] if n > 0 else 0
195
+ avg = sum(all_durations) / n if n > 0 else 0
196
+
197
+ # Top errors
198
+ errors = sorted(
199
+ [
200
+ {
201
+ "id": e.id,
202
+ "method": e.method,
203
+ "host": e.host,
204
+ "path": e.path,
205
+ "status": e.status_code,
206
+ }
207
+ for e in error_entries
208
+ if e.status_code >= 500
209
+ ],
210
+ key=lambda x: x["status"],
211
+ reverse=True,
212
+ )[:10]
213
+
214
+ return TrafficSummary(
215
+ total=len(entries),
216
+ hosts=hosts,
217
+ top_endpoints=endpoints,
218
+ status_distribution=dict(status_dist),
219
+ method_distribution=dict(method_dist),
220
+ protocol_distribution=dict(proto_dist),
221
+ avg_duration_ms=avg,
222
+ p95_duration_ms=p95,
223
+ p99_duration_ms=p99,
224
+ errors=errors,
225
+ )