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.
- proxyscope-0.1.0/LICENSE +21 -0
- proxyscope-0.1.0/PKG-INFO +165 -0
- proxyscope-0.1.0/README.md +144 -0
- proxyscope-0.1.0/proxyscope/__init__.py +2 -0
- proxyscope-0.1.0/proxyscope/app/__init__.py +0 -0
- proxyscope-0.1.0/proxyscope/app/config/__init__.py +0 -0
- proxyscope-0.1.0/proxyscope/app/config/matching.py +154 -0
- proxyscope-0.1.0/proxyscope/app/config/models.py +25 -0
- proxyscope-0.1.0/proxyscope/app/config/runtime.py +460 -0
- proxyscope-0.1.0/proxyscope/app/config/serialization.py +85 -0
- proxyscope-0.1.0/proxyscope/app/editing/__init__.py +0 -0
- proxyscope-0.1.0/proxyscope/app/editing/modifier.py +98 -0
- proxyscope-0.1.0/proxyscope/app/editing/policy.py +58 -0
- proxyscope-0.1.0/proxyscope/app/editing/response.py +274 -0
- proxyscope-0.1.0/proxyscope/app/logging/__init__.py +0 -0
- proxyscope-0.1.0/proxyscope/app/logging/observability.py +26 -0
- proxyscope-0.1.0/proxyscope/app/logging/request_response.py +179 -0
- proxyscope-0.1.0/proxyscope/app/logging/setup.py +11 -0
- proxyscope-0.1.0/proxyscope/app/main.py +90 -0
- proxyscope-0.1.0/proxyscope/app/runtime/__init__.py +0 -0
- proxyscope-0.1.0/proxyscope/app/runtime/cli.py +567 -0
- proxyscope-0.1.0/proxyscope/app/runtime/commands.py +419 -0
- proxyscope-0.1.0/proxyscope/app/runtime/input.py +151 -0
- proxyscope-0.1.0/proxyscope/app/runtime/journal.py +160 -0
- proxyscope-0.1.0/proxyscope/app/runtime/replay.py +134 -0
- proxyscope-0.1.0/proxyscope/app/runtime/tui/__init__.py +3 -0
- proxyscope-0.1.0/proxyscope/app/runtime/tui/renderer.py +531 -0
- proxyscope-0.1.0/proxyscope/mitm/__init__.py +0 -0
- proxyscope-0.1.0/proxyscope/mitm/certificates.py +149 -0
- proxyscope-0.1.0/proxyscope/mitm/tunnel.py +255 -0
- proxyscope-0.1.0/proxyscope/proxy/__init__.py +0 -0
- proxyscope-0.1.0/proxyscope/proxy/connect_tunnel.py +116 -0
- proxyscope-0.1.0/proxyscope/proxy/forwarding.py +76 -0
- proxyscope-0.1.0/proxyscope/proxy/http1_request_rewriter.py +167 -0
- proxyscope-0.1.0/proxyscope/proxy/http1_response_modifier_rewriter.py +232 -0
- proxyscope-0.1.0/proxyscope/proxy/http1_sniffer.py +162 -0
- proxyscope-0.1.0/proxyscope/proxy/http_bridge.py +35 -0
- proxyscope-0.1.0/proxyscope/proxy/server.py +383 -0
- proxyscope-0.1.0/proxyscope/proxy/tunnel_registry.py +33 -0
- proxyscope-0.1.0/proxyscope/proxy/types.py +8 -0
- proxyscope-0.1.0/pyproject.toml +34 -0
proxyscope-0.1.0/LICENSE
ADDED
|
@@ -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).
|
|
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
|