proxyscope 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. proxyscope-0.1.0/LICENSE +21 -0
  2. proxyscope-0.1.0/PKG-INFO +165 -0
  3. proxyscope-0.1.0/README.md +144 -0
  4. proxyscope-0.1.0/proxyscope/__init__.py +2 -0
  5. proxyscope-0.1.0/proxyscope/app/__init__.py +0 -0
  6. proxyscope-0.1.0/proxyscope/app/config/__init__.py +0 -0
  7. proxyscope-0.1.0/proxyscope/app/config/matching.py +154 -0
  8. proxyscope-0.1.0/proxyscope/app/config/models.py +25 -0
  9. proxyscope-0.1.0/proxyscope/app/config/runtime.py +460 -0
  10. proxyscope-0.1.0/proxyscope/app/config/serialization.py +85 -0
  11. proxyscope-0.1.0/proxyscope/app/editing/__init__.py +0 -0
  12. proxyscope-0.1.0/proxyscope/app/editing/modifier.py +98 -0
  13. proxyscope-0.1.0/proxyscope/app/editing/policy.py +58 -0
  14. proxyscope-0.1.0/proxyscope/app/editing/response.py +274 -0
  15. proxyscope-0.1.0/proxyscope/app/logging/__init__.py +0 -0
  16. proxyscope-0.1.0/proxyscope/app/logging/observability.py +26 -0
  17. proxyscope-0.1.0/proxyscope/app/logging/request_response.py +179 -0
  18. proxyscope-0.1.0/proxyscope/app/logging/setup.py +11 -0
  19. proxyscope-0.1.0/proxyscope/app/main.py +90 -0
  20. proxyscope-0.1.0/proxyscope/app/runtime/__init__.py +0 -0
  21. proxyscope-0.1.0/proxyscope/app/runtime/cli.py +567 -0
  22. proxyscope-0.1.0/proxyscope/app/runtime/commands.py +419 -0
  23. proxyscope-0.1.0/proxyscope/app/runtime/input.py +151 -0
  24. proxyscope-0.1.0/proxyscope/app/runtime/journal.py +160 -0
  25. proxyscope-0.1.0/proxyscope/app/runtime/replay.py +134 -0
  26. proxyscope-0.1.0/proxyscope/app/runtime/tui/__init__.py +3 -0
  27. proxyscope-0.1.0/proxyscope/app/runtime/tui/renderer.py +531 -0
  28. proxyscope-0.1.0/proxyscope/mitm/__init__.py +0 -0
  29. proxyscope-0.1.0/proxyscope/mitm/certificates.py +149 -0
  30. proxyscope-0.1.0/proxyscope/mitm/tunnel.py +255 -0
  31. proxyscope-0.1.0/proxyscope/proxy/__init__.py +0 -0
  32. proxyscope-0.1.0/proxyscope/proxy/connect_tunnel.py +116 -0
  33. proxyscope-0.1.0/proxyscope/proxy/forwarding.py +76 -0
  34. proxyscope-0.1.0/proxyscope/proxy/http1_request_rewriter.py +167 -0
  35. proxyscope-0.1.0/proxyscope/proxy/http1_response_modifier_rewriter.py +232 -0
  36. proxyscope-0.1.0/proxyscope/proxy/http1_sniffer.py +162 -0
  37. proxyscope-0.1.0/proxyscope/proxy/http_bridge.py +35 -0
  38. proxyscope-0.1.0/proxyscope/proxy/server.py +383 -0
  39. proxyscope-0.1.0/proxyscope/proxy/tunnel_registry.py +33 -0
  40. proxyscope-0.1.0/proxyscope/proxy/types.py +8 -0
  41. proxyscope-0.1.0/pyproject.toml +34 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tim Seyschab
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.
@@ -0,0 +1,165 @@
1
+ Metadata-Version: 2.4
2
+ Name: proxyscope
3
+ Version: 0.1.0
4
+ Summary: Interactive HTTP/HTTPS debugging proxy with runtime policies and terminal UI.
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Keywords: proxy,http,https,mitm,tui,debugging
8
+ Author: Tim Seyschab
9
+ Author-email: tim@shellnuts.de
10
+ Requires-Python: >=3.13,<4.0
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Topic :: Internet :: Proxy Servers
16
+ Classifier: Topic :: Software Development :: Testing
17
+ Classifier: Environment :: Console :: Curses
18
+ Requires-Dist: requests (>=2.32.0,<3.0.0)
19
+ Description-Content-Type: text/markdown
20
+
21
+ # proxyscope
22
+
23
+ `proxyscope` is an interactive HTTP/HTTPS proxy for request inspection, response editing, static-response policies, and live runtime control in a terminal UI.
24
+
25
+ ## Highlights
26
+
27
+ - Forward HTTP requests through an upstream connection.
28
+ - Handle HTTPS via `CONNECT` and optional TLS interception (MITM).
29
+ - Group request/response logs by request id.
30
+ - Inspect request/response details in a TUI.
31
+ - Edit matching responses in an external editor.
32
+ - Persist runtime configuration and policy rules.
33
+ - Replay captured requests.
34
+
35
+ ## Requirements
36
+
37
+ - Python `3.13+`
38
+ - Poetry `2.x`
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ poetry install
44
+ ```
45
+
46
+ ## Run
47
+
48
+ ```bash
49
+ poetry run proxyscope --host 127.0.0.1 --port 8080
50
+ ```
51
+
52
+ With persisted runtime configuration:
53
+
54
+ ```bash
55
+ poetry run proxyscope --config ./runtime-config.json
56
+ ```
57
+
58
+ Without TUI:
59
+
60
+ ```bash
61
+ poetry run proxyscope --no-ui
62
+ ```
63
+
64
+ ## Runtime Commands (TUI)
65
+
66
+ - `help`
67
+ - `clear`
68
+ - `sites`
69
+ - `loglevel <DEBUG|INFO|WARNING|ERROR>`
70
+ - `whitelist [show|add|remove|clear] ...`
71
+ - `cache [show|on|off|toggle]`
72
+ - `config [show|save [path]|reload]`
73
+ - `policy [show|add-editor|add-static|edit|remove|enable|disable] ...`
74
+ - `quit`
75
+
76
+ ## Keyboard Shortcuts (TUI)
77
+
78
+ - `Left` / `Right`: Focus zwischen Panels wechseln.
79
+ - `Up` / `Down`: Auswahl bewegen oder vertikal scrollen (abhängig vom aktiven Panel).
80
+ - `PageUp` / `PageDown`: Schnelles vertikales Scrollen.
81
+ - `Enter`: Request-Detail für den ausgewählten Request öffnen (oder Kommando ausführen, wenn Eingabezeile befüllt ist).
82
+ - `Backspace`: Zeichen in der Kommandozeile löschen.
83
+ - `Tab` / `Shift+Tab`: Im Request-Detail zwischen `Request` und `Response` Tab wechseln.
84
+ - `Shift+B`: Request-Detail schließen, zurück zur Request-Liste.
85
+ - `Shift+P`: Rechtes Panel auf `Policies` fokussieren.
86
+ - `Shift+S`: Rechtes Panel auf `Sites` fokussieren.
87
+ - `Shift+V`: Rechtes Panel ein-/ausblenden.
88
+ - `Shift+M`: Ausgewählten Request als Editor-Policy (`METHOD + URL`) hinzufügen.
89
+ - `Shift+R`: Ausgewählten Request editieren und erneut senden.
90
+ - `Shift+T`: Im Request-Detail zwischen `Request` und `Response` Tab wechseln.
91
+ - `Shift+A`: Ausgewählte Site zur Log-Whitelist hinzufügen (`Sites`-Tab).
92
+ - `Shift+D`: Ausgewählte Site aus der Log-Whitelist entfernen (`Sites`-Tab).
93
+ - `Shift+E`: Ausgewählte Policy aktivieren (`Policies`-Tab).
94
+ - `Shift+D`: Ausgewählte Policy deaktivieren (`Policies`-Tab).
95
+ - `Shift+I`: Ausgewählte Policy bearbeiten (`Policies`-Tab).
96
+ - `Shift+X`: Ausgewählte Policy entfernen (`Policies`-Tab).
97
+
98
+ ## TLS Interception (MITM)
99
+
100
+ Generate local CA materials:
101
+
102
+ ```bash
103
+ ./scripts/generate_mitm_ca.sh
104
+ ```
105
+
106
+ Import `certs/ca/mitm-ca.crt` into your browser trust store for local testing.
107
+
108
+ ## Tests
109
+
110
+ ```bash
111
+ poetry run python -m unittest discover -s tests -p "test_*.py" -v
112
+ ```
113
+
114
+ ## Release / Publish
115
+
116
+ ### 1. Bump Version
117
+
118
+ ```bash
119
+ poetry version patch
120
+ # or: poetry version minor / poetry version major
121
+ ```
122
+
123
+ ### 2. Validate + Build
124
+
125
+ ```bash
126
+ poetry check
127
+ poetry build
128
+ ```
129
+
130
+ ### 3. Configure PyPI Token
131
+
132
+ Either use an environment variable:
133
+
134
+ ```bash
135
+ export POETRY_PYPI_TOKEN_PYPI="pypi-..."
136
+ ```
137
+
138
+ or persistent Poetry config:
139
+
140
+ ```bash
141
+ poetry config pypi-token.pypi "pypi-..."
142
+ ```
143
+
144
+ ### 4. Publish to PyPI
145
+
146
+ ```bash
147
+ poetry publish --no-interaction
148
+ ```
149
+
150
+ Alternative:
151
+
152
+ ```bash
153
+ ./scripts/publish_pypi.sh
154
+ ```
155
+
156
+ ### 5. Install from PyPI (verification)
157
+
158
+ ```bash
159
+ pip install proxyscope
160
+ ```
161
+
162
+ ## License
163
+
164
+ MIT. See [LICENSE](LICENSE).
165
+
@@ -0,0 +1,144 @@
1
+ # proxyscope
2
+
3
+ `proxyscope` is an interactive HTTP/HTTPS proxy for request inspection, response editing, static-response policies, and live runtime control in a terminal UI.
4
+
5
+ ## Highlights
6
+
7
+ - Forward HTTP requests through an upstream connection.
8
+ - Handle HTTPS via `CONNECT` and optional TLS interception (MITM).
9
+ - Group request/response logs by request id.
10
+ - Inspect request/response details in a TUI.
11
+ - Edit matching responses in an external editor.
12
+ - Persist runtime configuration and policy rules.
13
+ - Replay captured requests.
14
+
15
+ ## Requirements
16
+
17
+ - Python `3.13+`
18
+ - Poetry `2.x`
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ poetry install
24
+ ```
25
+
26
+ ## Run
27
+
28
+ ```bash
29
+ poetry run proxyscope --host 127.0.0.1 --port 8080
30
+ ```
31
+
32
+ With persisted runtime configuration:
33
+
34
+ ```bash
35
+ poetry run proxyscope --config ./runtime-config.json
36
+ ```
37
+
38
+ Without TUI:
39
+
40
+ ```bash
41
+ poetry run proxyscope --no-ui
42
+ ```
43
+
44
+ ## Runtime Commands (TUI)
45
+
46
+ - `help`
47
+ - `clear`
48
+ - `sites`
49
+ - `loglevel <DEBUG|INFO|WARNING|ERROR>`
50
+ - `whitelist [show|add|remove|clear] ...`
51
+ - `cache [show|on|off|toggle]`
52
+ - `config [show|save [path]|reload]`
53
+ - `policy [show|add-editor|add-static|edit|remove|enable|disable] ...`
54
+ - `quit`
55
+
56
+ ## Keyboard Shortcuts (TUI)
57
+
58
+ - `Left` / `Right`: Focus zwischen Panels wechseln.
59
+ - `Up` / `Down`: Auswahl bewegen oder vertikal scrollen (abhängig vom aktiven Panel).
60
+ - `PageUp` / `PageDown`: Schnelles vertikales Scrollen.
61
+ - `Enter`: Request-Detail für den ausgewählten Request öffnen (oder Kommando ausführen, wenn Eingabezeile befüllt ist).
62
+ - `Backspace`: Zeichen in der Kommandozeile löschen.
63
+ - `Tab` / `Shift+Tab`: Im Request-Detail zwischen `Request` und `Response` Tab wechseln.
64
+ - `Shift+B`: Request-Detail schließen, zurück zur Request-Liste.
65
+ - `Shift+P`: Rechtes Panel auf `Policies` fokussieren.
66
+ - `Shift+S`: Rechtes Panel auf `Sites` fokussieren.
67
+ - `Shift+V`: Rechtes Panel ein-/ausblenden.
68
+ - `Shift+M`: Ausgewählten Request als Editor-Policy (`METHOD + URL`) hinzufügen.
69
+ - `Shift+R`: Ausgewählten Request editieren und erneut senden.
70
+ - `Shift+T`: Im Request-Detail zwischen `Request` und `Response` Tab wechseln.
71
+ - `Shift+A`: Ausgewählte Site zur Log-Whitelist hinzufügen (`Sites`-Tab).
72
+ - `Shift+D`: Ausgewählte Site aus der Log-Whitelist entfernen (`Sites`-Tab).
73
+ - `Shift+E`: Ausgewählte Policy aktivieren (`Policies`-Tab).
74
+ - `Shift+D`: Ausgewählte Policy deaktivieren (`Policies`-Tab).
75
+ - `Shift+I`: Ausgewählte Policy bearbeiten (`Policies`-Tab).
76
+ - `Shift+X`: Ausgewählte Policy entfernen (`Policies`-Tab).
77
+
78
+ ## TLS Interception (MITM)
79
+
80
+ Generate local CA materials:
81
+
82
+ ```bash
83
+ ./scripts/generate_mitm_ca.sh
84
+ ```
85
+
86
+ Import `certs/ca/mitm-ca.crt` into your browser trust store for local testing.
87
+
88
+ ## Tests
89
+
90
+ ```bash
91
+ poetry run python -m unittest discover -s tests -p "test_*.py" -v
92
+ ```
93
+
94
+ ## Release / Publish
95
+
96
+ ### 1. Bump Version
97
+
98
+ ```bash
99
+ poetry version patch
100
+ # or: poetry version minor / poetry version major
101
+ ```
102
+
103
+ ### 2. Validate + Build
104
+
105
+ ```bash
106
+ poetry check
107
+ poetry build
108
+ ```
109
+
110
+ ### 3. Configure PyPI Token
111
+
112
+ Either use an environment variable:
113
+
114
+ ```bash
115
+ export POETRY_PYPI_TOKEN_PYPI="pypi-..."
116
+ ```
117
+
118
+ or persistent Poetry config:
119
+
120
+ ```bash
121
+ poetry config pypi-token.pypi "pypi-..."
122
+ ```
123
+
124
+ ### 4. Publish to PyPI
125
+
126
+ ```bash
127
+ poetry publish --no-interaction
128
+ ```
129
+
130
+ Alternative:
131
+
132
+ ```bash
133
+ ./scripts/publish_pypi.sh
134
+ ```
135
+
136
+ ### 5. Install from PyPI (verification)
137
+
138
+ ```bash
139
+ pip install proxyscope
140
+ ```
141
+
142
+ ## License
143
+
144
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,2 @@
1
+ """tproxy package."""
2
+
File without changes
File without changes
@@ -0,0 +1,154 @@
1
+ from dataclasses import dataclass
2
+ from urllib.parse import urlsplit
3
+
4
+ from proxyscope.app.config.models import PolicyRule
5
+
6
+
7
+ def normalize_whitelist_entry(value: str) -> str:
8
+ raw_value = value.strip()
9
+ if not raw_value:
10
+ raise ValueError("Whitelist entry must not be empty.")
11
+
12
+ if "://" in raw_value:
13
+ parsed = urlsplit(raw_value)
14
+ host = parsed.hostname
15
+ elif "/" in raw_value:
16
+ parsed = urlsplit(f"http://{raw_value}")
17
+ host = parsed.hostname
18
+ else:
19
+ parsed = urlsplit(f"http://{raw_value}")
20
+ host = parsed.hostname
21
+
22
+ if not host:
23
+ raise ValueError(f"Invalid whitelist entry: {value}")
24
+ return host.lower()
25
+
26
+
27
+ def normalize_modification_url(value: str) -> str:
28
+ raw_value = value.strip()
29
+ if not raw_value:
30
+ raise ValueError("Modification URL must not be empty.")
31
+
32
+ if "://" not in raw_value:
33
+ raw_value = f"http://{raw_value}"
34
+
35
+ parsed = urlsplit(raw_value)
36
+ if not parsed.hostname:
37
+ raise ValueError(f"Invalid modification URL: {value}")
38
+
39
+ scheme = parsed.scheme.lower() or "http"
40
+ host = parsed.hostname.lower()
41
+ port = parsed.port
42
+ path = parsed.path or "/"
43
+ normalized_path = path if path.startswith("/") else f"/{path}"
44
+ netloc = f"{host}:{port}" if port is not None else host
45
+ return f"{scheme}://{netloc}{normalized_path}"
46
+
47
+
48
+ def normalize_http_method(method: str | None) -> str:
49
+ if method is None:
50
+ raise ValueError("HTTP method must not be empty.")
51
+ normalized = method.strip().upper()
52
+ if not normalized:
53
+ raise ValueError("HTTP method must not be empty.")
54
+ return normalized
55
+
56
+
57
+ def request_url_candidates(url: str) -> tuple[str, ...]:
58
+ normalized = normalize_modification_url(url)
59
+ alternate = _alternate_scheme_url(normalized)
60
+ if alternate is None:
61
+ return (normalized,)
62
+ return (normalized, alternate)
63
+
64
+
65
+ def policy_rule_matches_request(*, rule: PolicyRule, method: str, url_candidates: tuple[str, ...]) -> bool:
66
+ methods = rule.match.methods
67
+ if methods is not None and method not in methods:
68
+ return False
69
+
70
+ url_exact = rule.match.url_exact
71
+ url_prefix = rule.match.url_prefix
72
+ if url_exact is None and url_prefix is None:
73
+ return True
74
+
75
+ for candidate in url_candidates:
76
+ if url_exact is not None and candidate == url_exact:
77
+ return True
78
+ if url_prefix is not None and candidate.startswith(url_prefix):
79
+ return True
80
+ # Host-wide fallback when configured with "/"
81
+ candidate_parsed = _parse_normalized_modification_url(candidate)
82
+ if url_exact is not None:
83
+ entry = _parse_normalized_modification_url(url_exact)
84
+ if entry.host == candidate_parsed.host and entry.port == candidate_parsed.port:
85
+ if entry.path == "/" or candidate_parsed.path.startswith(entry.path):
86
+ return True
87
+ if url_prefix is not None:
88
+ entry = _parse_normalized_modification_url(url_prefix)
89
+ if entry.host == candidate_parsed.host and entry.port == candidate_parsed.port:
90
+ if entry.path == "/" or candidate_parsed.path.startswith(entry.path):
91
+ return True
92
+ return False
93
+
94
+
95
+ def rule_matches_url(rule: PolicyRule, normalized_url: str) -> bool:
96
+ target = rule.match.url_exact or rule.match.url_prefix
97
+ if target is None:
98
+ return False
99
+ return target == normalized_url
100
+
101
+
102
+ def first_rule_method(rule: PolicyRule) -> str | None:
103
+ methods = rule.match.methods
104
+ if not methods:
105
+ return None
106
+ return methods[0]
107
+
108
+
109
+ def rule_method_display(rule: PolicyRule) -> str:
110
+ method = first_rule_method(rule)
111
+ return method or "*"
112
+
113
+
114
+ def rule_url_display(rule: PolicyRule) -> str:
115
+ return rule.match.url_exact or rule.match.url_prefix or "*"
116
+
117
+
118
+ def policy_description(rule: PolicyRule) -> str:
119
+ state = "on" if rule.enabled else "off"
120
+ method = rule_method_display(rule)
121
+ target = rule_url_display(rule)
122
+ if rule.action == "open_editor":
123
+ return f"{rule.name} [{state}] open_editor {method} {target}"
124
+ if rule.action == "static_response" and rule.static_response is not None:
125
+ return (
126
+ f"{rule.name} [{state}] static_response {method} {target} "
127
+ f"-> {rule.static_response.status_code} {rule.static_response.reason}"
128
+ )
129
+ return f"{rule.name} [{state}] {rule.action} {method} {target}"
130
+
131
+
132
+ def _alternate_scheme_url(normalized_url: str) -> str | None:
133
+ parsed = urlsplit(normalized_url)
134
+ scheme = parsed.scheme.lower()
135
+ if scheme not in {"http", "https"}:
136
+ return None
137
+ alternate_scheme = "https" if scheme == "http" else "http"
138
+ return f"{alternate_scheme}://{parsed.netloc}{parsed.path or '/'}"
139
+
140
+
141
+ @dataclass(frozen=True)
142
+ class _NormalizedModUrl:
143
+ host: str
144
+ port: int | None
145
+ path: str
146
+
147
+
148
+ def _parse_normalized_modification_url(value: str) -> _NormalizedModUrl:
149
+ parsed = urlsplit(value)
150
+ return _NormalizedModUrl(
151
+ host=(parsed.hostname or "").lower(),
152
+ port=parsed.port,
153
+ path=parsed.path or "/",
154
+ )
@@ -0,0 +1,25 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass(frozen=True)
5
+ class RequestMatchRule:
6
+ methods: tuple[str, ...] | None = None
7
+ url_exact: str | None = None
8
+ url_prefix: str | None = None
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class StaticResponseTemplate:
13
+ status_code: int
14
+ reason: str
15
+ headers: dict[str, str]
16
+ body: bytes
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class PolicyRule:
21
+ name: str
22
+ enabled: bool
23
+ action: str # "open_editor" | "static_response"
24
+ match: RequestMatchRule
25
+ static_response: StaticResponseTemplate | None = None