Skip to content

Commit 88e418a

Browse files
Merge pull request #1 from LorcanChinnock/docs/readme-2026-rewrite
docs: rewrite README following 2026 OSS conventions
2 parents 9862850 + 4d9d81e commit 88e418a

1 file changed

Lines changed: 106 additions & 59 deletions

File tree

README.md

Lines changed: 106 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,131 @@
11
# aspire-tunnel-proxy
22

3-
A tiny, config-driven local reverse proxy with a stable public HTTPS URL. Edit a JSON file to declare what to forward where, run one command, share the link.
3+
[![CI](https://github.com/LorcanChinnock/aspire-tunnel-proxy/actions/workflows/ci.yml/badge.svg)](https://github.com/LorcanChinnock/aspire-tunnel-proxy/actions/workflows/ci.yml)
4+
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
5+
[![.NET 10](https://img.shields.io/badge/.NET-10.0-512BD4.svg)](https://dotnet.microsoft.com/download/dotnet/10.0)
46

5-
Built on [.NET Aspire](https://learn.microsoft.com/dotnet/aspire/) + [YARP](https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/) + [Microsoft Dev Tunnels](https://learn.microsoft.com/azure/developer/dev-tunnels/).
7+
A config-driven local reverse proxy with a stable public HTTPS URL. Edit one JSON file, run one command, share the link.
68

7-
## Prerequisites
9+
Built on [.NET Aspire](https://learn.microsoft.com/dotnet/aspire/), [YARP](https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/), and [Microsoft Dev Tunnels](https://learn.microsoft.com/azure/developer/dev-tunnels/).
810

9-
- **.NET 10 SDK**<https://dotnet.microsoft.com/download/dotnet/10.0>
10-
- **`devtunnel` CLI** — install per OS:
11-
- macOS: `brew install --cask devtunnel`
12-
- Windows: `winget install Microsoft.devtunnel`
13-
- Linux: `curl -sL https://aka.ms/DevTunnelCliInstall | bash`
14-
- More: <https://learn.microsoft.com/azure/developer/dev-tunnels/get-started#install>
15-
- A Microsoft or GitHub account (the `devtunnel` CLI prompts you to sign in on first run; Aspire handles this automatically).
11+
## Why this exists
1612

17-
## Run it
13+
You need a public HTTPS URL that points at something running on your laptop — to receive a webhook, demo a prototype, or test a mobile app against a local API. ngrok works, but you pay for stable URLs and lose control over headers, CORS, and request shaping. This project is the same idea, free, and you keep full control: add CORS, inject auth headers or JSON fields, hot-reload routes from a config file.
14+
15+
## Quickstart
1816

1917
```bash
20-
git clone <repo-url>
18+
git clone https://github.com/LorcanChinnock/aspire-tunnel-proxy.git
2119
cd aspire-tunnel-proxy
2220
dotnet run --project src/AppHost
2321
```
2422

25-
Works identically on macOS, Linux, and Windows (PowerShell or `cmd`). On first run, a browser window opens for `devtunnel` sign-in.
26-
27-
## Use it
23+
First run opens a browser for `devtunnel` sign-in. Then:
2824

2925
1. Open the Aspire dashboard URL printed in the terminal.
30-
2. Click the `tunnel` resource and copy its `https://aspire-tunnel-proxy-*.devtunnels.ms` URL.
31-
3. Edit `src/Proxy/appsettings.json` — point `Clusters.default.Destinations.primary.Address` at the URL you want to forward to. Save; YARP hot-reloads, no restart.
32-
4. Edit `src/AppHost/appsettings.json` — change `DevTunnel:Id` to your own globally-unique slug (a-z, 0-9, hyphen) so collaborators get their own stable URL.
26+
2. Click the `tunnel` resource — copy its `https://*.devtunnels.ms` URL.
27+
3. Edit `src/Proxy/appsettings.json` → set `Clusters.default.Destinations.primary.Address` to your upstream. YARP hot-reloads — no restart needed.
28+
4. Edit `src/AppHost/appsettings.json` → change `DevTunnel:Id` to a unique slug (a–z, 0–9, `-`) so collaborators get distinct stable URLs.
29+
30+
That's it. Anything hitting the tunnel URL is forwarded to your configured destination.
31+
32+
### Requirements
33+
34+
| | Version |
35+
|---|---|
36+
| .NET SDK | 10.0+ |
37+
| `devtunnel` CLI | latest ([install](https://learn.microsoft.com/azure/developer/dev-tunnels/get-started#install)) |
38+
| Account | Microsoft or GitHub (for `devtunnel` sign-in) |
39+
40+
`devtunnel` install one-liners — macOS: `brew install --cask devtunnel`; Windows: `winget install Microsoft.devtunnel`; Linux: `curl -sL https://aka.ms/DevTunnelCliInstall | bash`.
41+
42+
## Use cases
43+
44+
### Receive webhooks from third-party services
45+
46+
Stripe, GitHub, Slack, etc. need a public HTTPS endpoint to send events. Point the proxy at your local handler, paste the tunnel URL into the provider's webhook config.
47+
48+
```jsonc
49+
// src/Proxy/appsettings.json
50+
"Clusters": {
51+
"default": {
52+
"Destinations": {
53+
"primary": { "Address": "http://localhost:5050/" }
54+
}
55+
}
56+
}
57+
```
58+
59+
Now `https://<your-slug>.devtunnels.ms/stripe/events` reaches `http://localhost:5050/stripe/events`.
60+
61+
### Test a local API from a real mobile device
3362

34-
Optional features: [CORS](#cors), [request header injection](#header-injection), [JSON body field injection](#json-body-field-injection), [private tunnel](#security).
63+
Phones, tablets, and other dev machines can't reach `localhost:5000`. Run the proxy, give them the tunnel URL — works over cellular, hotel Wi-Fi, anywhere.
3564

36-
YARP route/cluster reference: <https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/config-files>.
65+
### Demo a prototype to a teammate
3766

38-
### CORS
67+
```bash
68+
dotnet run --project src/AppHost
69+
# share the printed devtunnels.ms URL
70+
```
71+
72+
Stable across restarts as long as `DevTunnel:Id` doesn't change.
73+
74+
### Add CORS to an upstream that doesn't support it
3975

40-
Declare named CORS policies under `Cors:Policies`, then reference them from any route via the standard YARP `CorsPolicy` field. The bundled default policy is wide-open:
76+
Wrap a bare API with browser-friendly CORS without modifying the upstream:
4177

42-
```json
78+
```jsonc
4379
"Cors": {
4480
"Policies": {
4581
"default": {
46-
"AllowedOrigins": ["*"],
47-
"AllowedMethods": ["*"],
82+
"AllowedOrigins": ["https://my-spa.example.com"],
83+
"AllowedMethods": ["GET", "POST"],
4884
"AllowedHeaders": ["*"],
49-
"ExposedHeaders": [],
50-
"AllowCredentials": false,
51-
"PreflightMaxAgeSeconds": null
85+
"AllowCredentials": false
86+
}
87+
}
88+
},
89+
"ReverseProxy": {
90+
"Routes": {
91+
"default": {
92+
"ClusterId": "default",
93+
"CorsPolicy": "default",
94+
"Match": { "Path": "{**catch-all}" }
5295
}
5396
}
5497
}
5598
```
5699

57-
- `["*"]` for `AllowedOrigins`, `AllowedMethods`, or `AllowedHeaders` enables the matching `AllowAny*` rule. An explicit list narrows the policy to those values.
58-
- `AllowCredentials: true` combined with `AllowedOrigins: ["*"]` is invalid — startup throws `InvalidOperationException` with the policy name. List explicit origins instead.
59-
- `PreflightMaxAgeSeconds` (optional) sets the `Access-Control-Max-Age` response header for preflight caching.
60-
- Routes opt in by setting `CorsPolicy: "<name>"`. ASP.NET Core CORS middleware short-circuits `OPTIONS` preflights so YARP never forwards them upstream.
61-
- CORS policies are read at startup. Editing `Cors:Policies` requires an AppHost restart. `Routes` and `Clusters` continue to hot-reload as before.
62-
63-
Reference: <https://learn.microsoft.com/aspnet/core/security/cors>.
100+
`["*"]` enables `AllowAny*`. Combining `AllowedOrigins: ["*"]` with `AllowCredentials: true` is invalid and throws at startup — list explicit origins instead.
64101

65-
### Header injection
102+
### Inject auth headers before forwarding
66103

67-
Add a `Transforms` array inside a route to set or append request headers before forwarding:
104+
Public-facing tunnel, secret-bearing upstream. Use route transforms:
68105

69-
```json
106+
```jsonc
70107
"Routes": {
71108
"default": {
72109
"ClusterId": "default",
73-
"CorsPolicy": "default",
74110
"Match": { "Path": "{**catch-all}" },
75111
"Transforms": [
76-
{ "RequestHeader": "X-Server-Secret", "Set": "your-secret-here" },
112+
{ "RequestHeader": "X-Api-Key", "Set": "your-secret-here" },
77113
{ "RequestHeader": "X-Trace", "Append": "proxy" }
78114
]
79115
}
80116
}
81117
```
82118

83-
- `Set` overwrites a client-supplied value of the same name.
84-
- `Append` adds a value without replacing existing ones (multiple values become comma-joined).
85-
- Client-supplied headers pass through by default.
119+
`Set` overwrites client-supplied values. `Append` adds without replacing.
86120

87-
Reference: <https://microsoft.github.io/reverse-proxy/articles/transforms-request.html>.
121+
### Inject auth fields into JSON request bodies
88122

89-
### JSON body field injection
123+
For upstreams that authenticate via body fields, not headers:
90124

91-
For upstreams that authenticate via JSON body fields rather than headers, declare the fields to merge under the route's `Metadata` block with the `InjectJsonField:` prefix:
92-
93-
```json
125+
```jsonc
94126
"Routes": {
95127
"default": {
96128
"ClusterId": "default",
97-
"CorsPolicy": "default",
98129
"Match": { "Path": "{**catch-all}" },
99130
"Metadata": {
100131
"InjectJsonField:AuthToken": "your-server-side-secret",
@@ -105,25 +136,41 @@ For upstreams that authenticate via JSON body fields rather than headers, declar
105136
}
106137
```
107138

108-
- Fields are merged into the top level of the JSON request body before forwarding.
109-
- Each value is parsed as JSON first: `"true"` becomes a boolean, `"42"` becomes a number, `"{\"k\":\"v\"}"` becomes a nested object. Anything that isn't valid JSON is injected as a raw string.
110-
- If a client-supplied field collides with an injected key, the injected value wins.
111-
- Skipped silently when the request `Content-Type` is not `application/json`, the body is empty, or the parsed JSON isn't an object — the body passes through unchanged so the upstream's own validation surfaces the error.
139+
Each value parses as JSON first — `"42"` becomes a number, `"true"` a boolean, `"{...}"` a nested object. Anything else is injected as a string. Skipped silently when the body isn't JSON, is empty, or isn't an object.
140+
141+
## Configuration reference
142+
143+
| File | Purpose |
144+
|---|---|
145+
| `src/AppHost/appsettings.json` | Tunnel ID, public/private toggle |
146+
| `src/Proxy/appsettings.json` | Routes, clusters, CORS policies, transforms |
147+
148+
YARP routes/clusters fully follow the upstream schema — see [YARP config files](https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/config-files).
149+
150+
**Hot reload:** `Routes` and `Clusters` reload on save with no restart. `Cors:Policies` are read at startup — changes need an AppHost restart.
112151

113152
## Security
114153

115-
`DevTunnel:AnonymousAccess: true` (the default) makes the tunnel URL **publicly reachable by anyone who knows it**. Don't proxy anything with secrets, dev databases, or unauthenticated admin surfaces.
154+
`DevTunnel:AnonymousAccess: true` (the default) makes the tunnel URL **publicly reachable by anyone who knows it**. Don't proxy anything with secrets, dev databases, or unauthenticated admin surfaces over an anonymous tunnel.
155+
156+
Set `DevTunnel:AnonymousAccess: false` in `src/AppHost/appsettings.json` for a private tunnel. Recipients then need a Microsoft/GitHub login the owner has authorised, or an `X-Tunnel-Authorization` token from `devtunnel token`. Note: private tunnels block cross-origin browser callers — `fetch()` from a deployed SPA on another origin can't complete the interactive sign-in.
116157

117-
To make the tunnel private, set `DevTunnel:AnonymousAccess` to `false` in `src/AppHost/appsettings.json`. Recipients then need a Microsoft or GitHub login the tunnel owner has authorised, or an `X-Tunnel-Authorization` token issued by `devtunnel token`. Note: a private tunnel breaks cross-origin browser callers — `fetch()` from a deployed SPA on another origin cannot complete the interactive sign-in flow.
158+
To report a vulnerability privately, please open a [GitHub security advisory](https://github.com/LorcanChinnock/aspire-tunnel-proxy/security/advisories/new) rather than a public issue.
118159

119-
## Layout
160+
## Project layout
120161

121162
```
122163
src/
123-
├── AppHost/ .NET Aspire app host: wires the proxy to a Dev Tunnel
124-
└── Proxy/ ASP.NET Core + YARP: routes/clusters live in appsettings.json
164+
├── AppHost/ .NET Aspire app host — wires the proxy to a Dev Tunnel
165+
└── Proxy/ ASP.NET Core + YARP — routes/clusters live in appsettings.json
166+
tests/
167+
└── Proxy.Tests/ xUnit v3 integration + unit tests
125168
```
126169

170+
## Contributing
171+
172+
Issues and pull requests welcome. PRs run a build + test gate via [GitHub Actions](.github/workflows/ci.yml) and require one CODEOWNER review before merge.
173+
127174
## License
128175

129-
MIT — see [LICENSE](LICENSE).
176+
[MIT](LICENSE).

0 commit comments

Comments
 (0)