azure-dev-proxy 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.
- azure_dev_proxy-0.1.0/.github/workflows/publish.yml +64 -0
- azure_dev_proxy-0.1.0/.github/workflows/tests.yml +29 -0
- azure_dev_proxy-0.1.0/.gitignore +10 -0
- azure_dev_proxy-0.1.0/LICENSE +21 -0
- azure_dev_proxy-0.1.0/PKG-INFO +8 -0
- azure_dev_proxy-0.1.0/README.md +258 -0
- azure_dev_proxy-0.1.0/azure_dev_proxy/__init__.py +1 -0
- azure_dev_proxy-0.1.0/azure_dev_proxy/__main__.py +104 -0
- azure_dev_proxy-0.1.0/azure_dev_proxy/auth.py +382 -0
- azure_dev_proxy-0.1.0/azure_dev_proxy/config.py +367 -0
- azure_dev_proxy-0.1.0/azure_dev_proxy/default-config.json +34 -0
- azure_dev_proxy-0.1.0/azure_dev_proxy/idle_timer.py +62 -0
- azure_dev_proxy-0.1.0/azure_dev_proxy/listeners.py +108 -0
- azure_dev_proxy-0.1.0/azure_dev_proxy/proxy.py +478 -0
- azure_dev_proxy-0.1.0/azure_dev_proxy/routers/__init__.py +3 -0
- azure_dev_proxy-0.1.0/azure_dev_proxy/routers/_base.py +31 -0
- azure_dev_proxy-0.1.0/azure_dev_proxy/routers/graphiql.py +96 -0
- azure_dev_proxy-0.1.0/azure_dev_proxy/routers/management.py +247 -0
- azure_dev_proxy-0.1.0/azure_dev_proxy/routers/reverse_proxy.py +174 -0
- azure_dev_proxy-0.1.0/azure_dev_proxy/routers/utils.py +220 -0
- azure_dev_proxy-0.1.0/azure_dev_proxy/static/app.js +124 -0
- azure_dev_proxy-0.1.0/azure_dev_proxy/static/graphiql.html +109 -0
- azure_dev_proxy-0.1.0/azure_dev_proxy/static/index.html +90 -0
- azure_dev_proxy-0.1.0/azure_dev_proxy/static/login.html +26 -0
- azure_dev_proxy-0.1.0/azure_dev_proxy/tokens.py +242 -0
- azure_dev_proxy-0.1.0/pyproject.toml +52 -0
- azure_dev_proxy-0.1.0/tests/conftest.py +42 -0
- azure_dev_proxy-0.1.0/tests/fake_az.py +19 -0
- azure_dev_proxy-0.1.0/tests/test_auth.py +773 -0
- azure_dev_proxy-0.1.0/tests/test_config.py +457 -0
- azure_dev_proxy-0.1.0/tests/test_features.py +1116 -0
- azure_dev_proxy-0.1.0/tests/test_proxy.py +1823 -0
- azure_dev_proxy-0.1.0/tests/test_tokens.py +478 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
name: Publish tagged releases to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
tests:
|
|
13
|
+
uses: ./.github/workflows/tests.yml
|
|
14
|
+
|
|
15
|
+
build:
|
|
16
|
+
needs: tests
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v6
|
|
20
|
+
- name: Set up Python
|
|
21
|
+
uses: actions/setup-python@v6
|
|
22
|
+
with:
|
|
23
|
+
python-version: "3.14"
|
|
24
|
+
- name: Check release version
|
|
25
|
+
env:
|
|
26
|
+
RELEASE_TAG: ${{ github.ref_name }}
|
|
27
|
+
run: |
|
|
28
|
+
python - <<'PY'
|
|
29
|
+
import os
|
|
30
|
+
import tomllib
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
version = tomllib.loads(Path("pyproject.toml").read_text())["project"]["version"]
|
|
34
|
+
expected = f"v{version}"
|
|
35
|
+
if os.environ["RELEASE_TAG"] != expected:
|
|
36
|
+
raise SystemExit(f"Release tag must be {expected}")
|
|
37
|
+
PY
|
|
38
|
+
- name: Install Hatch
|
|
39
|
+
run: python -m pip install hatch
|
|
40
|
+
- name: Build distributions
|
|
41
|
+
run: hatch build
|
|
42
|
+
- name: Upload distributions
|
|
43
|
+
uses: actions/upload-artifact@v6
|
|
44
|
+
with:
|
|
45
|
+
name: python-distributions
|
|
46
|
+
path: dist/
|
|
47
|
+
if-no-files-found: error
|
|
48
|
+
|
|
49
|
+
publish:
|
|
50
|
+
needs: build
|
|
51
|
+
runs-on: ubuntu-latest
|
|
52
|
+
environment:
|
|
53
|
+
name: pypi
|
|
54
|
+
url: https://pypi.org/p/azure-dev-proxy
|
|
55
|
+
permissions:
|
|
56
|
+
id-token: write
|
|
57
|
+
steps:
|
|
58
|
+
- name: Download distributions
|
|
59
|
+
uses: actions/download-artifact@v6
|
|
60
|
+
with:
|
|
61
|
+
name: python-distributions
|
|
62
|
+
path: dist/
|
|
63
|
+
- name: Publish to PyPI
|
|
64
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: Run tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- "**"
|
|
7
|
+
pull_request:
|
|
8
|
+
workflow_call:
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
test:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
strategy:
|
|
17
|
+
fail-fast: false
|
|
18
|
+
matrix:
|
|
19
|
+
python-version: ["3.11", "3.12", "3.13", "3.14"]
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v6
|
|
22
|
+
- name: Set up Python
|
|
23
|
+
uses: actions/setup-python@v6
|
|
24
|
+
with:
|
|
25
|
+
python-version: ${{ matrix.python-version }}
|
|
26
|
+
- name: Install Hatch
|
|
27
|
+
run: python -m pip install hatch
|
|
28
|
+
- name: Run tests and lint
|
|
29
|
+
run: hatch run tests
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Matthew Wardrop
|
|
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,258 @@
|
|
|
1
|
+
# Azure Dev Proxy
|
|
2
|
+
|
|
3
|
+
A reverse proxy that allows you to open APIs or webpages using your existing
|
|
4
|
+
Azure CLI sign-in. It forwards requests through a local loopback endpoint and
|
|
5
|
+
injects `Authorization: Bearer` tokens for configured HTTPS destinations.
|
|
6
|
+
|
|
7
|
+
The browser UI provides URL navigation, host-to-token-audience mappings, and
|
|
8
|
+
GraphiQL. Command-line clients use the same forwarding routes. HTTP destinations
|
|
9
|
+
are supported, but never receive an Azure token.
|
|
10
|
+
|
|
11
|
+
## Get started
|
|
12
|
+
|
|
13
|
+
You need Python 3.11 or newer and Azure CLI available as `az` on your path.
|
|
14
|
+
Install from PyPI, sign in, and start the proxy as a non-root user:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
python -m pip install azure-dev-proxy
|
|
18
|
+
az login
|
|
19
|
+
azure-dev-proxy
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The proxy listens on loopback interfaces at port 8080, not on the local network.
|
|
23
|
+
Shared-key authentication protects access to the proxy separately from upstream
|
|
24
|
+
Azure authentication. On first use, it creates an `auth-token` file beside its
|
|
25
|
+
configuration and reuses the key across restarts.
|
|
26
|
+
|
|
27
|
+
### Browser
|
|
28
|
+
|
|
29
|
+
1. Open a ready-to-use URL from the startup logs, or visit
|
|
30
|
+
`http://localhost:8080/` and enter the key.
|
|
31
|
+
2. Add a [host mapping](#configure-host-mappings) if your service is not covered
|
|
32
|
+
by the defaults.
|
|
33
|
+
3. Paste the destination URL and select **Open URL**.
|
|
34
|
+
|
|
35
|
+
### curl
|
|
36
|
+
|
|
37
|
+
With the proxy running, open another terminal. In a POSIX shell, replace
|
|
38
|
+
`/path/to/auth-token` with the key-file path printed at startup and send a GET
|
|
39
|
+
request:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
PROXY_KEY=$(cat /path/to/auth-token)
|
|
43
|
+
|
|
44
|
+
curl --fail-with-body \
|
|
45
|
+
-H "X-Azure-Dev-Proxy-Token: $PROXY_KEY" \
|
|
46
|
+
"http://127.0.0.1:8080/https://api.example.com/path"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
To POST JSON:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
curl --fail-with-body \
|
|
53
|
+
-H "X-Azure-Dev-Proxy-Token: $PROXY_KEY" \
|
|
54
|
+
-H "Content-Type: application/json" \
|
|
55
|
+
--data '{"name":"example"}' \
|
|
56
|
+
"http://127.0.0.1:8080/https://api.example.com/items"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Replace the example URLs with your API and configure a matching host mapping
|
|
60
|
+
if it needs an Azure token. If you use `--config` or `--token-file`, read the
|
|
61
|
+
key file shown at startup instead.
|
|
62
|
+
|
|
63
|
+
The header authenticates to the local proxy; the host mapping selects the
|
|
64
|
+
upstream bearer token. Embed the destination URL in the request path rather
|
|
65
|
+
than using `curl --proxy`. Use `127.0.0.1` to avoid the `localhost` redirect.
|
|
66
|
+
|
|
67
|
+
> [!WARNING]
|
|
68
|
+
> **Keep the proxy key and startup logs private.** Startup URLs intentionally
|
|
69
|
+
> include the key for convenient sign-in. Anyone with the key and access to the
|
|
70
|
+
> local listener can use your Azure identity through the proxy. Keys can also
|
|
71
|
+
> appear in browser history, command traces, or process listings. The proxy does
|
|
72
|
+
> not isolate malicious applications running as your own user.
|
|
73
|
+
|
|
74
|
+
## Configure host mappings
|
|
75
|
+
|
|
76
|
+
A host mapping allowlists an HTTPS destination for token injection and selects
|
|
77
|
+
the token resource, or audience, requested through Azure CLI. Use the audience
|
|
78
|
+
required by the API; it may differ from the API's URL.
|
|
79
|
+
|
|
80
|
+
On `localhost`, expand **Host mappings and temporary overrides**, enter a
|
|
81
|
+
hostname and resource, and select **Save temporary override**. A blank resource
|
|
82
|
+
uses the destination's HTTPS origin.
|
|
83
|
+
|
|
84
|
+
Temporary rules apply immediately and disappear on process exit. **Save to
|
|
85
|
+
persisted** writes a rule to the configuration file; **Delete persisted**
|
|
86
|
+
removes a saved rule. Persisted edits require a writable file and apply without
|
|
87
|
+
a restart. Failed writes leave the active rules unchanged.
|
|
88
|
+
|
|
89
|
+
Alternatively, supply JSON:
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"token_rules": [
|
|
94
|
+
{"host": "api.example.com", "resource": "https://identity.example.com"},
|
|
95
|
+
{"host": "*.example.org"}
|
|
96
|
+
],
|
|
97
|
+
"trusted_origins": []
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
azure-dev-proxy --config ./config.json
|
|
103
|
+
azure-dev-proxy --config ./config.json --check-config
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Replace the placeholder domains with your service settings. Exact hosts take
|
|
107
|
+
priority over wildcards; the longest wildcard suffix wins. Wildcards match
|
|
108
|
+
subdomains, not the bare domain. A missing or `null` resource uses the
|
|
109
|
+
destination's HTTPS origin. Unmatched hosts are forwarded without an Azure
|
|
110
|
+
token. Temporary overrides take priority over persisted rules.
|
|
111
|
+
|
|
112
|
+
Configuration uses the native per-user directory:
|
|
113
|
+
|
|
114
|
+
| Platform | Default configuration file |
|
|
115
|
+
|---|---|
|
|
116
|
+
| Linux | `$XDG_CONFIG_HOME/azure-dev-proxy/config.json`, or `~/.config/azure-dev-proxy/config.json` when unset. |
|
|
117
|
+
| Windows | `%APPDATA%\azure-dev-proxy\config.json`, or `~\AppData\Roaming\azure-dev-proxy\config.json` when unset. |
|
|
118
|
+
| macOS | `~/Library/Application Support/azure-dev-proxy/config.json` |
|
|
119
|
+
|
|
120
|
+
The generated `auth-token` lives beside the configuration unless `--token-file`
|
|
121
|
+
is supplied. `XDG_CONFIG_HOME` applies only on Linux and other non-Windows,
|
|
122
|
+
non-macOS platforms. Missing files receive bundled defaults; existing files
|
|
123
|
+
define the complete rule list and are never merged with updated defaults.
|
|
124
|
+
|
|
125
|
+
On Windows and macOS, existing files under `~/.config` are not moved
|
|
126
|
+
automatically. Use `--config` with the old path to retain those settings and
|
|
127
|
+
the adjacent key, or move both files to the native directory.
|
|
128
|
+
|
|
129
|
+
### Bundled mappings
|
|
130
|
+
|
|
131
|
+
The [default configuration](azure_dev_proxy/default-config.json) includes
|
|
132
|
+
public-cloud mappings for management, storage, secrets, messaging, analytics,
|
|
133
|
+
AI, monitoring, and productivity APIs. It is a curated allowlist, not a
|
|
134
|
+
wildcard for every Azure-hosted service. Your signed-in user still needs the
|
|
135
|
+
service's permissions and consent; supply any additional API-required headers.
|
|
136
|
+
|
|
137
|
+
Event Hubs and Service Bus share `*.servicebus.windows.net` but require
|
|
138
|
+
different audiences. The default selects Service Bus. For Event Hubs, add an
|
|
139
|
+
exact hostname override with resource `https://eventhubs.azure.net`.
|
|
140
|
+
|
|
141
|
+
## Forwarding and GraphQL
|
|
142
|
+
|
|
143
|
+
Forwarding routes embed the upstream URL in the local path:
|
|
144
|
+
|
|
145
|
+
```text
|
|
146
|
+
http://127.0.0.1:8080/https://api.example.com/path
|
|
147
|
+
http://127.0.0.1:8080/http://localhost:4000/path
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The URL-entry page defaults to HTTPS when the scheme is omitted. Forwarding
|
|
151
|
+
routes on `localhost` redirect to `127.0.0.1`, separating upstream pages from
|
|
152
|
+
the configuration editor. Only `localhost` permits configuration edits;
|
|
153
|
+
IP-loopback origins expose read-only mappings.
|
|
154
|
+
|
|
155
|
+
For GraphiQL, open:
|
|
156
|
+
|
|
157
|
+
```text
|
|
158
|
+
http://localhost:8080/:graphiql/https://api.example.com/graphql
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Browser applications
|
|
162
|
+
|
|
163
|
+
To permit cross-origin forwarding from a local application, add its exact
|
|
164
|
+
origin to `trusted_origins`, for example `["http://localhost:3000"]`. Include
|
|
165
|
+
nondefault ports; paths, wildcards, and opaque (`null`) origins are rejected.
|
|
166
|
+
Only trusted origins receive forwarding CORS headers.
|
|
167
|
+
|
|
168
|
+
The application must also authenticate with `X-Azure-Dev-Proxy-Token`; origin
|
|
169
|
+
trust does not replace authentication or enable cross-site session cookies.
|
|
170
|
+
By default, only the proxy's own loopback origins are trusted.
|
|
171
|
+
|
|
172
|
+
**Trust only applications and forwarded pages that may use your Azure
|
|
173
|
+
identity.** Forwarded pages share the IP-loopback origin; host mappings do not
|
|
174
|
+
isolate those pages from one another.
|
|
175
|
+
|
|
176
|
+
## Security and sessions
|
|
177
|
+
|
|
178
|
+
Azure tokens are injected only for mapped HTTPS destinations. Redirects are
|
|
179
|
+
evaluated independently, and HTTP forwarding is always anonymous. Caller
|
|
180
|
+
`Authorization` headers, cookies, and proxy credentials are stripped upstream.
|
|
181
|
+
Request and response bodies are streamed without automatic decompression.
|
|
182
|
+
|
|
183
|
+
Browser-origin checks run before forwarding or token acquisition. Configuration
|
|
184
|
+
mutations additionally require same-origin access and an anti-CSRF token.
|
|
185
|
+
Browsers must support Fetch Metadata. Clients without browser provenance
|
|
186
|
+
headers are treated as CLI clients but still require proxy authentication.
|
|
187
|
+
|
|
188
|
+
The reserved `azure-dev-proxy-token=KEY` query parameter also authenticates
|
|
189
|
+
requests and establishes a session. It is stripped before forwarding.
|
|
190
|
+
Browser navigations redirect to a key-free URL; API requests receive their
|
|
191
|
+
response directly. Other query parameters retain their encoding and order.
|
|
192
|
+
|
|
193
|
+
Sessions use HttpOnly, SameSite=Lax, host-only cookies. A short-lived, single-use
|
|
194
|
+
handoff pairs sessions across `localhost` and `127.0.0.1`. Sessions can survive
|
|
195
|
+
restarts with the same key and port; rotate the key and restart to invalidate
|
|
196
|
+
them.
|
|
197
|
+
|
|
198
|
+
On POSIX, the key must be a regular file owned by the current user with no
|
|
199
|
+
group or other permissions. Symlinks are rejected, and validation and reading
|
|
200
|
+
use the same file descriptor to prevent pathname-replacement races. On Windows,
|
|
201
|
+
protect the containing directory's inherited permissions. Request logs omit
|
|
202
|
+
the reserved key parameter; startup logs intentionally include it.
|
|
203
|
+
|
|
204
|
+
## Runtime options
|
|
205
|
+
|
|
206
|
+
| Option | Purpose |
|
|
207
|
+
|---|---|
|
|
208
|
+
| `--port 8080` | Set the loopback port. |
|
|
209
|
+
| `--az-path /path/to/az` | Select the Azure CLI executable; defaults to `az` on `PATH`. |
|
|
210
|
+
| `--config FILE` | Select a JSON configuration file. |
|
|
211
|
+
| `--check-config` | Validate configuration without listening; creates defaults if missing. |
|
|
212
|
+
| `--idle-timeout 300` | Exit after five idle minutes; default `0` disables idle shutdown. |
|
|
213
|
+
| `--token-file FILE` | Select a protected key file. |
|
|
214
|
+
| `--token KEY` | Supply the proxy key directly. |
|
|
215
|
+
| `--no-auth` | Disable proxy authentication and key-file creation. |
|
|
216
|
+
|
|
217
|
+
`AZURE_DEV_PROXY_TOKEN` also supplies a key; explicit `--token` or
|
|
218
|
+
`--token-file` options take precedence. **With `--no-auth`, other local users
|
|
219
|
+
and applications can use the proxy without a key.**
|
|
220
|
+
|
|
221
|
+
Active requests and streams prevent idle shutdown; open pages and unused
|
|
222
|
+
keep-alive connections do not. Temporary rules and acquired tokens are
|
|
223
|
+
memory-only.
|
|
224
|
+
|
|
225
|
+
Systemd socket activation accepts one or two loopback TCP listeners through
|
|
226
|
+
`LISTEN_PID` and `LISTEN_FDS`; their ports must match `--port`. Otherwise, the
|
|
227
|
+
proxy binds IPv4 and available IPv6 loopback interfaces.
|
|
228
|
+
|
|
229
|
+
## Development
|
|
230
|
+
|
|
231
|
+
From a local checkout:
|
|
232
|
+
|
|
233
|
+
```sh
|
|
234
|
+
python -m pip install -e .
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Install [Hatch](https://hatch.pypa.io/latest/install/), then run:
|
|
238
|
+
|
|
239
|
+
```sh
|
|
240
|
+
hatch run tests
|
|
241
|
+
hatch run format
|
|
242
|
+
hatch build
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
`tests` runs pytest with a 100% Python line-and-branch coverage gate, including
|
|
246
|
+
CLI subprocesses, followed by Ruff lint and format checks and ty type checking.
|
|
247
|
+
`format` applies Ruff fixes and formatting. For a focused run, use
|
|
248
|
+
`hatch run unit-test -k token -v`.
|
|
249
|
+
|
|
250
|
+
Use `hatch run coverage` for the coverage report alone. While adding tests,
|
|
251
|
+
`hatch run coverage --cov-fail-under=0` reports remaining gaps without enforcing
|
|
252
|
+
the gate. Add `--cov-report=html` for a browsable report in `htmlcov/`.
|
|
253
|
+
|
|
254
|
+
The import namespace is `azure_dev_proxy`. Configuration, authentication, token
|
|
255
|
+
acquisition, and listeners are separate modules. `Proxy` loads configuration,
|
|
256
|
+
initializes the token provider and local authentication, and owns the aiohttp
|
|
257
|
+
application and lifecycle, with routes in `azure_dev_proxy.routers`. Callers
|
|
258
|
+
can also supply configuration, a token provider, or authentication explicitly.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Local Azure-authenticated HTTPS reverse proxy."""
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .config import Config
|
|
9
|
+
from .proxy import Proxy
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def port_number(value: str) -> int:
|
|
13
|
+
try:
|
|
14
|
+
port = int(value)
|
|
15
|
+
except ValueError as error:
|
|
16
|
+
raise argparse.ArgumentTypeError("Port must be an integer") from error
|
|
17
|
+
if not 1 <= port <= 65535:
|
|
18
|
+
raise argparse.ArgumentTypeError("Port must be between 1 and 65535")
|
|
19
|
+
return port
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def main() -> None:
|
|
23
|
+
"""
|
|
24
|
+
Run the loopback proxy or validate its configuration without listening.
|
|
25
|
+
|
|
26
|
+
POSIX supports inherited systemd listeners; otherwise the service binds its
|
|
27
|
+
own loopback sockets. Windows uses direct listeners and native signal handling.
|
|
28
|
+
"""
|
|
29
|
+
parser = argparse.ArgumentParser(
|
|
30
|
+
prog="azure-dev-proxy",
|
|
31
|
+
description="Local HTTP(S) proxy with HTTPS-only Azure CLI tokens",
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument("--version", action="version", version="azure-dev-proxy 0.1.0")
|
|
34
|
+
parser.add_argument(
|
|
35
|
+
"--port", type=port_number, default=8080, help="Loopback port (default: 8080)"
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"--az-path",
|
|
39
|
+
default="az",
|
|
40
|
+
help="Azure CLI executable path (default: az on PATH)",
|
|
41
|
+
)
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"--config",
|
|
44
|
+
type=Path,
|
|
45
|
+
help="JSON configuration (default: the user's azure-dev-proxy/config.json)",
|
|
46
|
+
)
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
"--idle-timeout",
|
|
49
|
+
type=idle_seconds,
|
|
50
|
+
default=0,
|
|
51
|
+
help="Opt-in idle shutdown seconds (default: 0, disabled)",
|
|
52
|
+
)
|
|
53
|
+
auth = parser.add_mutually_exclusive_group()
|
|
54
|
+
auth.add_argument(
|
|
55
|
+
"--token", help="Proxy authentication key (or AZURE_DEV_PROXY_TOKEN)"
|
|
56
|
+
)
|
|
57
|
+
auth.add_argument(
|
|
58
|
+
"--token-file",
|
|
59
|
+
type=Path,
|
|
60
|
+
help="Saved key file (default: auth-token beside the configuration)",
|
|
61
|
+
)
|
|
62
|
+
auth.add_argument(
|
|
63
|
+
"--no-auth", action="store_true", help="Disable proxy authentication"
|
|
64
|
+
)
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"--check-config",
|
|
67
|
+
action="store_true",
|
|
68
|
+
help="Validate configuration and exit without listening",
|
|
69
|
+
)
|
|
70
|
+
args = parser.parse_args()
|
|
71
|
+
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
|
|
72
|
+
try:
|
|
73
|
+
if not args.check_config and hasattr(os, "geteuid") and os.geteuid() == 0:
|
|
74
|
+
raise ValueError("Run azure-dev-proxy as a non-root user")
|
|
75
|
+
if args.check_config:
|
|
76
|
+
Config.load(
|
|
77
|
+
args.config if args.config is not None else Config.default_path()
|
|
78
|
+
)
|
|
79
|
+
return
|
|
80
|
+
proxy = Proxy(
|
|
81
|
+
config_path=args.config,
|
|
82
|
+
az_path=args.az_path,
|
|
83
|
+
token=args.token,
|
|
84
|
+
token_file=args.token_file,
|
|
85
|
+
no_auth=args.no_auth,
|
|
86
|
+
)
|
|
87
|
+
proxy.serve(args.port, idle_timeout=args.idle_timeout)
|
|
88
|
+
except (OSError, ValueError, RuntimeError) as error:
|
|
89
|
+
parser.exit(1, f"Could not start azure-dev-proxy: {error}\n")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def idle_seconds(value: str) -> int:
|
|
93
|
+
"""Parse nonnegative idle seconds; zero disables automatic shutdown."""
|
|
94
|
+
try:
|
|
95
|
+
seconds = int(value)
|
|
96
|
+
except ValueError:
|
|
97
|
+
raise argparse.ArgumentTypeError("Idle timeout must be an integer") from None
|
|
98
|
+
if seconds < 0:
|
|
99
|
+
raise argparse.ArgumentTypeError("Idle timeout must be nonnegative")
|
|
100
|
+
return seconds
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
if __name__ == "__main__":
|
|
104
|
+
main()
|